diff --git a/src/slp/nft1.js b/src/slp/nft1.js index 13b4ffe..9255bc3 100644 --- a/src/slp/nft1.js +++ b/src/slp/nft1.js @@ -45,7 +45,7 @@ class TokenType1 { configObj.documentHash, 0, configObj.mintBatonVout, - new slpMdm.BN("1") + new slpMdm.BN(configObj.initialQty) ) return script @@ -95,7 +95,7 @@ class TokenType1 { } } - generateNFTChildOpReturn(configObj) { + generateNFTChildGenesisOpReturn(configObj) { try { // TODO: Add input validation. @@ -117,7 +117,107 @@ class TokenType1 { return script } catch (err) { - console.log(`Error in generateNFTChildOpReturn()`) + console.log(`Error in generateNFTChildGenesisOpReturn()`) + throw err + } + } + + // Generate the OP_RETURN for sending an NFT Child token. + // Assumes all tokenUtxos have the same tokenId in common. It does not filter + // the input. + generateNFTChildSendOpReturn(tokenUtxos, sendQty) { + try { + // TODO: Add input validation. + + const tokenId = tokenUtxos[0].tokenId + + // Calculate the total amount of tokens owned by the wallet. + let totalTokens = 0 + for (let i = 0; i < tokenUtxos.length; i++) + totalTokens += tokenUtxos[i].tokenQty + + const change = totalTokens - sendQty + + let script + let outputs = 1 + + // The normal case, when there is token change to return to sender. + if (change > 0) { + outputs = 2 + + // Convert to integer string. + const sendStr = Math.floor(sendQty).toString() + const changeStr = Math.floor(change).toString() + + // Generate the OP_RETURN as a Buffer. + script = slpMdm.NFT1.Child.send(tokenId, [ + new slpMdm.BN(sendStr), + new slpMdm.BN(changeStr) + ]) + // + + // Corner case, when there is no token change to send back. + } else { + // Convert to integer string. + const sendStr = Math.floor(sendQty).toString() + + // Generate the OP_RETURN as a Buffer. + script = slpMdm.NFT1.Child.send(tokenId, [new slpMdm.BN(sendStr)]) + } + + return { script, outputs } + } catch (err) { + console.log(`Error in generateNFTChildSendOpReturn()`) + throw err + } + } + + // Generate the OP_RETURN for sending an NFT Group token. + // Assumes all tokenUtxos have the same tokenId in common, it does not filter + // the input. + generateNFTGroupSendOpReturn(tokenUtxos, sendQty) { + try { + // TODO: Add input validation. + + const tokenId = tokenUtxos[0].tokenId + + // Calculate the total amount of tokens owned by the wallet. + let totalTokens = 0 + for (let i = 0; i < tokenUtxos.length; i++) + totalTokens += tokenUtxos[i].tokenQty + + const change = totalTokens - sendQty + + let script + let outputs = 1 + + // The normal case, when there is token change to return to sender. + if (change > 0) { + outputs = 2 + + // Convert to integer string. + const sendStr = Math.floor(sendQty).toString() + const changeStr = Math.floor(change).toString() + + // Generate the OP_RETURN as a Buffer. + script = slpMdm.NFT1.Group.send(tokenId, [ + new slpMdm.BN(sendStr), + new slpMdm.BN(changeStr) + ]) + // + + // Corner case, when there is no token change to send back. + } else { + // Convert to integer string. + const sendStr = Math.floor(sendQty).toString() + + // Generate the OP_RETURN as a Buffer. + script = slpMdm.NFT1.Group.send(tokenId, [new slpMdm.BN(sendStr)]) + } + + return { script, outputs } + } catch (err) { + console.log(`Error in generateNFTGroupSendOpReturn()`) throw err } } diff --git a/src/slp/utils.js b/src/slp/utils.js index 8f59d8d..4a42a65 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1125,7 +1125,7 @@ class Utils { let slpData = false try { slpData = await this.decodeOpReturn(utxo.txid) - // console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`) + console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`) } catch (err) { // console.log(`error: `, err) // An error will be thrown if the txid is not SLP. @@ -1187,7 +1187,7 @@ class Utils { // If UTXO passes validation, then return formatted token data. else { const genesisData = await this.decodeOpReturn(slpData.tokenId) - // console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`) + console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`) // Minting Baton if (utxo.vout === slpData.mintBatonVout) { @@ -1265,7 +1265,7 @@ class Utils { // Finally, validate the SLP txid with SLPDB. if (outAry[i]) { const isValid = await this.validateTxid(utxo.txid) - // console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`) + console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`) outAry[i].isValid = isValid[0].valid } diff --git a/test/unit/slp-nft1.js b/test/unit/slp-nft1.js index 46ac0f6..9e9640b 100644 --- a/test/unit/slp-nft1.js +++ b/test/unit/slp-nft1.js @@ -44,7 +44,8 @@ describe("#SLP NFT1", () => { name: "SLP Test Token", ticker: "SLPTEST", documentUrl: "https://bchjs.cash", - documentHash: "" + documentHash: "", + initialQty: 5 } const result = bchjs.SLP.NFT1.newNFTGroupOpReturn(configObj) @@ -118,10 +119,146 @@ describe("#SLP NFT1", () => { documentHash: "" } - const result = bchjs.SLP.NFT1.generateNFTChildOpReturn(configObj) + const result = bchjs.SLP.NFT1.generateNFTChildGenesisOpReturn(configObj) // console.log(`result: `, result) assert.equal(Buffer.isBuffer(result), true) }) }) + + describe("#generateNFTChildSendOpReturn", () => { + it("should generate send OP_RETURN code for no change", () => { + // Mock UTXO. + const tokenUtxos = [ + { + txid: + "81955624a8eb7011769ff5faa607f78f84b0f8152b9145e7db8b1e521c895ee3", + vout: 1, + value: "546", + height: 638273, + confirmations: 42, + satoshis: 546, + utxoType: "token", + tokenQty: 1, + tokenId: + "81955624a8eb7011769ff5faa607f78f84b0f8152b9145e7db8b1e521c895ee3", + tokenTicker: "NFTC", + tokenName: "NFT Child", + tokenDocumentUrl: "https://FullStack.cash", + tokenDocumentHash: "", + decimals: 0, + tokenType: 129, + isValid: true + } + ] + + const result = bchjs.SLP.NFT1.generateNFTChildSendOpReturn(tokenUtxos, 1) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, ["script", "outputs"]) + assert.isNumber(result.outputs) + }) + + it("should generate send OP_RETURN code with change", () => { + // Mock UTXO. + const tokenUtxos = [ + { + txid: + "81955624a8eb7011769ff5faa607f78f84b0f8152b9145e7db8b1e521c895ee3", + vout: 1, + value: "546", + height: 638273, + confirmations: 42, + satoshis: 546, + utxoType: "token", + tokenQty: 4, + tokenId: + "81955624a8eb7011769ff5faa607f78f84b0f8152b9145e7db8b1e521c895ee3", + tokenTicker: "NFTC", + tokenName: "NFT Child", + tokenDocumentUrl: "https://FullStack.cash", + tokenDocumentHash: "", + decimals: 0, + tokenType: 129, + isValid: true + } + ] + + const result = bchjs.SLP.NFT1.generateNFTChildSendOpReturn(tokenUtxos, 1) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, ["script", "outputs"]) + assert.isNumber(result.outputs) + }) + }) + + describe("#generateNFTGroupSendOpReturn", () => { + it("should generate send OP_RETURN with change", () => { + // Mock UTXO. + const tokenUtxos = [ + { + txid: + "35846676e7514658bbd2fd60b1f0d4d86195908f6b2de5328d54c8e4a2d05919", + vout: 1, + value: "546", + height: 638207, + confirmations: 3, + satoshis: 546, + utxoType: "token", + tokenQty: 10, + transactionType: "mint", + tokenId: + "eee4b82e4bb7113eca433829144363fc45f110693c286494fbf5b5c8043cc981", + tokenType: 129, + tokenTicker: "NFTTT", + tokenName: "NFT Test Token", + tokenDocumentUrl: "https://FullStack.cash", + tokenDocumentHash: "", + decimals: 0, + mintBatonVout: 2, + isValid: true + } + ] + + const result = bchjs.SLP.NFT1.generateNFTGroupSendOpReturn(tokenUtxos, 1) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, ["script", "outputs"]) + assert.isNumber(result.outputs) + }) + + it("should generate send OP_RETURN with no change", () => { + // Mock UTXO. + const tokenUtxos = [ + { + txid: + "35846676e7514658bbd2fd60b1f0d4d86195908f6b2de5328d54c8e4a2d05919", + vout: 1, + value: "546", + height: 638207, + confirmations: 3, + satoshis: 546, + utxoType: "token", + tokenQty: 10, + transactionType: "mint", + tokenId: + "eee4b82e4bb7113eca433829144363fc45f110693c286494fbf5b5c8043cc981", + tokenType: 129, + tokenTicker: "NFTTT", + tokenName: "NFT Test Token", + tokenDocumentUrl: "https://FullStack.cash", + tokenDocumentHash: "", + decimals: 0, + mintBatonVout: 2, + isValid: true + } + ] + + const result = bchjs.SLP.NFT1.generateNFTGroupSendOpReturn(tokenUtxos, 10) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, ["script", "outputs"]) + assert.isNumber(result.outputs) + }) + }) }) diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index f87c763..4de4f98 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -1490,7 +1490,7 @@ describe("#SLP Utils", () => { assert2.equal(false, data[2]) }) - it("should decode a NFT Parent transaction", async () => { + it("should decode a NFT Group Genesis transaction", async () => { // Define stubbed data. const slpData = { tokenType: 129, @@ -1579,6 +1579,171 @@ describe("#SLP Utils", () => { assert2.equal(data[2].utxoType, "token") assert2.equal(data[2].tokenType, 129) }) + + it("should decode a NFT Group Mint transaction", async () => { + // Define stubbed data. + const slpData = { + tokenType: 129, + txType: "MINT", + tokenId: + "eee4b82e4bb7113eca433829144363fc45f110693c286494fbf5b5c8043cc981", + mintBatonVout: 2, + qty: "10" + } + + const genesisData = { + tokenType: 129, + txType: "GENESIS", + ticker: "NFTTT", + name: "NFT Test Token", + tokenId: + "eee4b82e4bb7113eca433829144363fc45f110693c286494fbf5b5c8043cc981", + documentUri: "https://FullStack.cash", + documentHash: "", + decimals: 0, + mintBatonVout: 2, + qty: "1" + } + + const stubValid = [ + { + txid: + "35846676e7514658bbd2fd60b1f0d4d86195908f6b2de5328d54c8e4a2d05919", + valid: true + } + ] + + // Mock external dependencies. + // Stub the calls to decodeOpReturn. + sandbox + .stub(slp.Utils, "decodeOpReturn") + .resolves(slpData) + .onCall(1) + .resolves(slpData) + .onCall(2) + .resolves(genesisData) + .onCall(3) + .resolves(slpData) + .onCall(4) + .resolves(genesisData) + + // Stub the call to validateTxid + sandbox + .stub(slp.Utils, "validateTxid") + .resolves(stubValid) + .onCall(1) + .resolves(stubValid) + .onCall(2) + .resolves(stubValid) + + const utxos = [ + { + txid: + "35846676e7514658bbd2fd60b1f0d4d86195908f6b2de5328d54c8e4a2d05919", + vout: 3, + value: "15620", + height: 638207, + confirmations: 3, + satoshis: 15620 + }, + { + txid: + "35846676e7514658bbd2fd60b1f0d4d86195908f6b2de5328d54c8e4a2d05919", + vout: 2, + value: "546", + height: 638207, + confirmations: 3, + satoshis: 546 + }, + { + txid: + "35846676e7514658bbd2fd60b1f0d4d86195908f6b2de5328d54c8e4a2d05919", + vout: 1, + value: "546", + height: 638207, + confirmations: 3, + satoshis: 546 + } + ] + + const data = await slp.Utils.tokenUtxoDetails(utxos) + console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert2.isArray(data) + assert2.equal(data[0], false) + + assert2.equal(data[1].utxoType, "minting-baton") + assert2.equal(data[1].tokenType, 129) + + assert2.equal(data[2].utxoType, "token") + assert2.equal(data[2].tokenType, 129) + }) + + it("should decode a NFT Child Genesis transaction", async () => { + // Define stubbed data. + const slpData = { + tokenType: 129, + txType: "GENESIS", + ticker: "NFTC", + name: "NFT Child", + tokenId: + "81955624a8eb7011769ff5faa607f78f84b0f8152b9145e7db8b1e521c895ee3", + documentUri: "https://FullStack.cash", + documentHash: "", + decimals: 0, + mintBatonVout: 0, + qty: "1" + } + + const stubValid = [ + { + txid: + "81955624a8eb7011769ff5faa607f78f84b0f8152b9145e7db8b1e521c895ee3", + valid: true + } + ] + + // Mock external dependencies. + // Stub the calls to decodeOpReturn. + sandbox + .stub(slp.Utils, "decodeOpReturn") + .resolves(slpData) + .onCall(1) + .resolves(slpData) + + // Stub the call to validateTxid + sandbox.stub(slp.Utils, "validateTxid").resolves(stubValid) + + const utxos = [ + { + txid: + "81955624a8eb7011769ff5faa607f78f84b0f8152b9145e7db8b1e521c895ee3", + vout: 2, + value: "16712", + height: 638273, + confirmations: 42, + satoshis: 16712 + }, + { + txid: + "81955624a8eb7011769ff5faa607f78f84b0f8152b9145e7db8b1e521c895ee3", + vout: 1, + value: "546", + height: 638273, + confirmations: 42, + satoshis: 546 + } + ] + + const data = await slp.Utils.tokenUtxoDetails(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert2.isArray(data) + assert2.equal(data[0], false) + + assert2.equal(data[1].utxoType, "token") + assert2.equal(data[1].tokenType, 129) + }) }) describe("#txDetails", () => {