diff --git a/cashtab/package-lock.json b/cashtab/package-lock.json --- a/cashtab/package-lock.json +++ b/cashtab/package-lock.json @@ -1,12 +1,12 @@ { "name": "cashtab", - "version": "2.29.2", + "version": "2.29.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cashtab", - "version": "2.29.2", + "version": "2.29.3", "dependencies": { "@bitgo/utxo-lib": "^9.33.0", "@zxing/browser": "^0.1.4", diff --git a/cashtab/package.json b/cashtab/package.json --- a/cashtab/package.json +++ b/cashtab/package.json @@ -1,6 +1,6 @@ { "name": "cashtab", - "version": "2.29.2", + "version": "2.29.3", "private": true, "scripts": { "start": "node scripts/start.js", diff --git a/cashtab/src/slpv1/__tests__/index.test.js b/cashtab/src/slpv1/__tests__/index.test.js --- a/cashtab/src/slpv1/__tests__/index.test.js +++ b/cashtab/src/slpv1/__tests__/index.test.js @@ -12,6 +12,7 @@ getMintBatons, getMintTargetOutputs, getMaxMintAmount, + getNftParentGenesisTargetOutputs, } from 'slpv1'; import vectors from '../fixtures/vectors'; import { SEND_DESTINATION_ADDRESS } from '../fixtures/vectors'; @@ -386,4 +387,29 @@ }); }); }); + describe('Get targetOutputs for NFT1 parent genesis tx', () => { + const { expectedReturns, expectedErrors } = + vectors.getNftParentGenesisTargetOutputs; + + // Successfully created targetOutputs + expectedReturns.forEach(expectedReturn => { + const { description, genesisConfig, targetOutputs } = + expectedReturn; + it(`getNftParentGenesisTargetOutputs: ${description}`, () => { + expect(getNftParentGenesisTargetOutputs(genesisConfig)).toEqual( + targetOutputs, + ); + }); + }); + + // Error cases + expectedErrors.forEach(expectedError => { + const { description, genesisConfig, errorMsg } = expectedError; + it(`getNftParentGenesisTargetOutputs throws error for: ${description}`, () => { + expect(() => + getNftParentGenesisTargetOutputs(genesisConfig), + ).toThrow(errorMsg); + }); + }); + }); }); diff --git a/cashtab/src/slpv1/fixtures/vectors.js b/cashtab/src/slpv1/fixtures/vectors.js --- a/cashtab/src/slpv1/fixtures/vectors.js +++ b/cashtab/src/slpv1/fixtures/vectors.js @@ -1245,4 +1245,127 @@ }, ], }, + getNftParentGenesisTargetOutputs: { + expectedReturns: [ + { + description: 'Fixed supply NFT1 parent', + genesisConfig: { + name: 'NFT1 Parent Test', + ticker: 'NPT', + documentUrl: 'https://cashtab.com/', + initialQty: '100', + documentHash: + '0000000000000000108da5cf31407c9261d489171db51a88cc400c7590eb087c', + mintBatonVout: null, + }, + targetOutputs: [ + { + value: 0, + script: Buffer.from( + '6a04534c500001810747454e45534953034e5054104e46543120506172656e7420546573741468747470733a2f2f636173687461622e636f6d2f200000000000000000108da5cf31407c9261d489171db51a88cc400c7590eb087c01004c00080000000000000064', + 'hex', + ), + }, + { + value: appConfig.etokenSats, + }, + ], + }, + { + description: 'Variable supply NFT1 parent', + genesisConfig: { + name: 'NFT1 Parent Test', + ticker: 'NPT', + documentUrl: 'https://cashtab.com/', + initialQty: '100', + documentHash: + '0000000000000000108da5cf31407c9261d489171db51a88cc400c7590eb087c', + mintBatonVout: 2, + }, + targetOutputs: [ + { + value: 0, + script: Buffer.from( + '6a04534c500001810747454e45534953034e5054104e46543120506172656e7420546573741468747470733a2f2f636173687461622e636f6d2f200000000000000000108da5cf31407c9261d489171db51a88cc400c7590eb087c01000102080000000000000064', + 'hex', + ), + }, + { + value: appConfig.etokenSats, + }, + { + value: appConfig.etokenSats, + }, + ], + }, + { + description: 'NFT1 parent genesis at max supply', + genesisConfig: { + name: 'NFT1 Parent Test', + ticker: 'NPT', + documentUrl: 'https://cashtab.com/', + initialQty: MAX_MINT_AMOUNT_TOKEN_SATOSHIS, + documentHash: + '0000000000000000108da5cf31407c9261d489171db51a88cc400c7590eb087c', + mintBatonVout: null, + }, + targetOutputs: [ + { + value: 0, + script: Buffer.from( + '6a04534c500001810747454e45534953034e5054104e46543120506172656e7420546573741468747470733a2f2f636173687461622e636f6d2f200000000000000000108da5cf31407c9261d489171db51a88cc400c7590eb087c01004c0008ffffffffffffffff', + 'hex', + ), + }, + { + value: appConfig.etokenSats, + }, + ], + }, + ], + expectedErrors: [ + { + description: + 'Variable supply NFT1 parent with mintBatonVout !== 2', + genesisConfig: { + name: 'NFT1 Parent Test', + ticker: 'NPT', + documentUrl: 'https://cashtab.com/', + initialQty: '100', + documentHash: + '0000000000000000108da5cf31407c9261d489171db51a88cc400c7590eb087c', + mintBatonVout: 3, + }, + errorMsg: + 'Cashtab only supports slpv1 genesis txs for fixed supply tokens or tokens with mint baton at index 2', + }, + { + description: 'Exceed 0xffffffffffffffff for genesis qty', + genesisConfig: { + name: 'NFT1 Parent Test', + ticker: 'NPT', + documentUrl: 'https://cashtab.com/', + initialQty: `${MAX_MINT_AMOUNT_TOKEN_SATOSHIS}1`, + documentHash: + '0000000000000000108da5cf31407c9261d489171db51a88cc400c7590eb087c', + mintBatonVout: null, + }, + mintAddress: GENESIS_MINT_ADDRESS, + errorMsg: 'bn outside of range', + }, + { + description: 'Initial qty is not an integer', + genesisConfig: { + name: 'NFT1 Parent Test', + ticker: 'NPT', + documentUrl: 'https://cashtab.com/', + initialQty: new BN(100.123), + documentHash: + '0000000000000000108da5cf31407c9261d489171db51a88cc400c7590eb087c', + mintBatonVout: null, + }, + errorMsg: 'bn not an integer', + }, + ], + }, }; diff --git a/cashtab/src/slpv1/index.js b/cashtab/src/slpv1/index.js --- a/cashtab/src/slpv1/index.js +++ b/cashtab/src/slpv1/index.js @@ -2,7 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -import { BN, TokenType1 } from 'slp-mdm'; +import { BN, TokenType1, NFT1 } from 'slp-mdm'; import appConfig from 'config/app'; import { initializeScript } from 'opreturn'; import { opReturn } from 'config/opreturn'; @@ -12,6 +12,10 @@ // 0xffffffffffffffff export const MAX_MINT_AMOUNT_TOKEN_SATOSHIS = '18446744073709551615'; +// Note: NFT1 spec supports non-zero decimals, but 0 is recommended +// Cashtab follows the recommendation and will only mint 0-decimal NFT1 parent tokens +const NFT1_PARENT_DECIMALS = 0; + /** * Get targetOutput for a SLP v1 genesis tx * @param {object} genesisConfig object containing token info for genesis tx @@ -423,3 +427,62 @@ ); return `${stringBeforeDecimalPoint}.${stringAfterDecimalPoint}`; }; + +/** + * Get targetOutput for a SLP v1 NFT Parent (aka Group) genesis tx + * @param {object} genesisConfig object containing token info for genesis tx + * @throws {error} if invalid input params are passed to TokenType1.genesis + * @returns {object} targetOutput, e.g. {value: 0, script: } + */ +export const getNftParentGenesisTargetOutputs = genesisConfig => { + const { + ticker, + name, + documentUrl, + documentHash, + mintBatonVout, + initialQty, + } = genesisConfig; + + if (mintBatonVout !== null && mintBatonVout !== 2) { + throw new Error( + 'Cashtab only supports slpv1 genesis txs for fixed supply tokens or tokens with mint baton at index 2', + ); + } + + const targetOutputs = []; + + // Note that this function handles validation; will throw an error on invalid inputs + const script = NFT1.Group.genesis( + ticker, + name, + documentUrl, + documentHash, + NFT1_PARENT_DECIMALS, + mintBatonVout, + // Per spec, this must be BN of an integer + // This function will throw an error if initialQty is not an integer + new BN(initialQty), + ); + + // Per SLP v1 spec, OP_RETURN must be at index 0 + // https://github.com/simpleledger/slp-specifications/blob/master/slp-token-type-1.md#genesis---token-genesis-transaction + targetOutputs.push({ value: 0, script }); + + // Per SLP v1 spec, genesis tx is minted to output at index 1 + // In Cashtab, we mint genesis txs to our own Path1899 address + // If an output does not have an address, Cashtab will add its change address + targetOutputs.push({ + value: appConfig.etokenSats, + }); + + // If the user specified the creation of a mint baton, add it + // Note: Cashtab only supports the creation of one mint baton at index 2 + if (mintBatonVout !== null) { + targetOutputs.push({ + value: appConfig.etokenSats, + }); + } + + return targetOutputs; +};