diff --git a/src/slp/utils.js b/src/slp/utils.js index 0cbb832..8f59d8d 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1167,6 +1167,7 @@ class Utils { utxo.tokenDocumentUrl = slpData.documentUri utxo.tokenDocumentHash = slpData.documentHash utxo.decimals = slpData.decimals + utxo.tokenType = slpData.tokenType // something outAry[i] = utxo @@ -1201,6 +1202,7 @@ class Utils { // Hydrate the UTXO object with information about the SLP token. utxo.transactionType = "mint" utxo.tokenId = slpData.tokenId + utxo.tokenType = slpData.tokenType utxo.tokenTicker = genesisData.ticker utxo.tokenName = genesisData.name @@ -1249,6 +1251,7 @@ class Utils { utxo.tokenDocumentUrl = genesisData.documentUri utxo.tokenDocumentHash = genesisData.documentHash utxo.decimals = genesisData.decimals + utxo.tokenType = slpData.tokenType // Calculate the real token quantity. utxo.tokenQty = tokenQty / Math.pow(10, genesisData.decimals) diff --git a/test/unit/fixtures/slp/mock-utils.js b/test/unit/fixtures/slp/mock-utils.js index ea1b233..8e145f0 100644 --- a/test/unit/fixtures/slp/mock-utils.js +++ b/test/unit/fixtures/slp/mock-utils.js @@ -1031,6 +1031,82 @@ const mockInvalidSlpSend = { blocktime: 1535577007 } +const txDetailsSLPNftGenesis = { + txid: "4ef6eb92950a13a69e97c2c02c7967d806aa874c0e2a6b5546a8880f2cd14bc4", + hash: "4ef6eb92950a13a69e97c2c02c7967d806aa874c0e2a6b5546a8880f2cd14bc4", + version: 2, + size: 344, + locktime: 0, + vin: [ + { + txid: "4041bc842554b796de1dbb625bbb6994379bc5132a47522677bef7323f3e5051", + vout: 2, + scriptSig: { + asm: + "3045022100f8d678e7cd6a2fc317fe4c0c8b633f5941aa40bceb6d7472559955908869544b02206d82e45a6658bb9b5ad39734aa93832b1de2d3c9650e1b59c2ee2f15aa26f3bb[ALL|FORKID] 029547345d63f86ea89ea92ea4ed26386c4493ff60c78d871cc7b38f517f3fd72c", + hex: + "483045022100f8d678e7cd6a2fc317fe4c0c8b633f5941aa40bceb6d7472559955908869544b02206d82e45a6658bb9b5ad39734aa93832b1de2d3c9650e1b59c2ee2f15aa26f3bb4121029547345d63f86ea89ea92ea4ed26386c4493ff60c78d871cc7b38f517f3fd72c" + }, + sequence: 4294967295 + } + ], + vout: [ + { + value: 0, + n: 0, + scriptPubKey: { + asm: + "OP_RETURN 5262419 -1 47454e45534953 4e46545454 4e4654205465737420546f6b656e 68747470733a2f2f46756c6c537461636b2e63617368 0 0 2 0000000000000001", + hex: + "6a04534c500001810747454e45534953054e465454540e4e4654205465737420546f6b656e1668747470733a2f2f46756c6c537461636b2e636173684c0001000102080000000000000001", + type: "nulldata" + } + }, + { + value: 0.00000546, + n: 1, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 6011206cd60db8b634f85edf46da22a6d1351e54 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a9146011206cd60db8b634f85edf46da22a6d1351e5488ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: ["bitcoincash:qpspzgrv6cxm3d35lp0d73k6y2ndzdg72s2304ttr8"] + } + }, + { + value: 0.00000546, + n: 2, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 6011206cd60db8b634f85edf46da22a6d1351e54 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a9146011206cd60db8b634f85edf46da22a6d1351e5488ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: ["bitcoincash:qpspzgrv6cxm3d35lp0d73k6y2ndzdg72s2304ttr8"] + } + }, + { + value: 0.0001562, + n: 3, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 6011206cd60db8b634f85edf46da22a6d1351e54 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a9146011206cd60db8b634f85edf46da22a6d1351e5488ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: ["bitcoincash:qpspzgrv6cxm3d35lp0d73k6y2ndzdg72s2304ttr8"] + } + } + ], + hex: + "020000000151503e3f32f7be772652472a13c59b379469bb5b62bb1dde96b7542584bc4140020000006b483045022100f8d678e7cd6a2fc317fe4c0c8b633f5941aa40bceb6d7472559955908869544b02206d82e45a6658bb9b5ad39734aa93832b1de2d3c9650e1b59c2ee2f15aa26f3bb4121029547345d63f86ea89ea92ea4ed26386c4493ff60c78d871cc7b38f517f3fd72cffffffff0400000000000000004b6a04534c500001810747454e45534953054e465454540e4e4654205465737420546f6b656e1668747470733a2f2f46756c6c537461636b2e636173684c000100010208000000000000000122020000000000001976a9146011206cd60db8b634f85edf46da22a6d1351e5488ac22020000000000001976a9146011206cd60db8b634f85edf46da22a6d1351e5488ac043d0000000000001976a9146011206cd60db8b634f85edf46da22a6d1351e5488ac00000000", + blockhash: "000000000000000002405a44302888bc29bf0e9f4d99b97303038bcc83c15e33", + confirmations: 2, + time: 1591329189, + blocktime: 1591329189 +} + module.exports = { mockList, mockToken, @@ -1051,6 +1127,7 @@ module.exports = { txDetailsSLPSend, txDetailsSLPGenesisNoBaton, txDetailsSLPSendAlt, + txDetailsSLPNftGenesis, mockTxDetails, mockDualValidation, mockDualOpData, diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index 9923956..f87c763 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -354,177 +354,6 @@ describe("#SLP Utils", () => { }) }) - // describe("#decodeOpReturn", () => { - // it("should throw an error for a non-string input", async () => { - // try { - // const txid = 53423 // Not a string. - // - // await slp.Utils.decodeOpReturn(txid) - // - // assert2.equal(true, false, "Unexpected result.") - // } catch (err) { - // //console.log(`err: ${util.inspect(err)}`) - // assert2.include(err.message, `txid string must be included`) - // } - // }) - // - // it("should throw an error for non-SLP transaction", async () => { - // try { - // // Mock the call to the REST API - // if (process.env.TEST === "unit") { - // sandbox - // .stub(axios, "get") - // .resolves({ data: mockData.nonSLPTxDetailsWithoutOpReturn }) - // } - // - // const txid = - // "3793d4906654f648e659f384c0f40b19c8f10c1e9fb72232a9b8edd61abaa1ec" - // - // await slp.Utils.decodeOpReturn(txid) - // - // assert2.equal(true, false, "Unexpected result.") - // } catch (err) { - // assert2.include(err.message, `Not an OP_RETURN`) - // //console.log(`err: ${util.inspect(err)}`) - // } - // }) - // - // it("should throw an error for non-SLP transaction with OP_RETURN", async () => { - // try { - // // Mock the call to the REST API - // if (process.env.TEST === "unit") { - // sandbox - // .stub(axios, "get") - // .resolves({ data: mockData.nonSLPTxDetailsWithOpReturn }) - // } - // - // const txid = - // "2ff74c48a5d657cf45f699601990bffbbe7a2a516d5480674cbf6c6a4497908f" - // - // await slp.Utils.decodeOpReturn(txid) - // - // assert2.equal(true, false, "Unexpected result.") - // } catch (err) { - // assert2.include(err.message, `Not a SLP OP_RETURN`) - // //console.log(`err: ${util.inspect(err)}`) - // } - // }) - // - // it("should decode a genesis transaction", async () => { - // // Mock the call to the REST API - // if (process.env.TEST === "unit") { - // sandbox - // .stub(axios, "get") - // .resolves({ data: mockData.txDetailsSLPGenesis }) - // } - // - // const txid = - // "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90" - // - // const data = await slp.Utils.decodeOpReturn(txid) - // //console.log(`data: ${JSON.stringify(data, null, 2)}`) - // - // assert2.hasAnyKeys(data, [ - // "tokenType", - // "transactionType", - // "ticker", - // "name", - // "documentUrl", - // "documentHash", - // "decimals", - // "mintBatonVout", - // "initialQty", - // "tokensSentTo", - // "batonHolder" - // ]) - // }) - // - // it("should decode a mint transaction", async () => { - // // Mock the call to the REST API - // if (process.env.TEST === "unit") - // sandbox.stub(axios, "get").resolves({ data: mockData.txDetailsSLPMint }) - // - // const txid = - // "65f21bbfcd545e5eb515e38e861a9dfe2378aaa2c4e458eb9e59e4d40e38f3a4" - // - // const data = await slp.Utils.decodeOpReturn(txid) - // //console.log(`data: ${JSON.stringify(data, null, 2)}`) - // - // assert2.hasAnyKeys(data, [ - // "transactionType", - // "tokenType", - // "tokenId", - // "mintBatonVout", - // "batonStillExists", - // "quantity", - // "tokensSentTo", - // "batonHolder" - // ]) - // }) - // - // it("should decode a send transaction", async () => { - // // Mock the call to the REST API - // if (process.env.TEST === "unit") - // sandbox.stub(axios, "get").resolves({ data: mockData.txDetailsSLPSend }) - // - // const txid = - // "4f922565af664b6fdf0a1ba3924487344be721b3d8815c62cafc8a51e04a8afa" - // - // const data = await slp.Utils.decodeOpReturn(txid) - // //console.log(`data: ${JSON.stringify(data, null, 2)}`) - // - // assert2.hasAnyKeys(data, [ - // "transactionType", - // "tokenType", - // "tokenId", - // "spendData" - // ]) - // assert2.isArray(data.spendData) - // assert2.hasAnyKeys(data.spendData[0], ["quantity", "sentTo", "vout"]) - // }) - // - // it("should properly decode a Genesis transaction with no minting baton", async () => { - // // Mock the call to the REST API. - // if (process.env.TEST === "unit") { - // sandbox - // .stub(axios, "get") - // .resolves({ data: mockData.txDetailsSLPGenesisNoBaton }) - // } - // - // const txid = - // "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7" - // - // const data = await slp.Utils.decodeOpReturn(txid) - // //console.log(`data: ${JSON.stringify(data, null, 2)}`) - // - // assert2.include(data.batonHolder, "NEVER_CREATED") - // }) - // - // it("should decode a send transaction with alternate encoding", async () => { - // // Mock the call to rest.bitcoin.com - // if (process.env.TEST === "unit") { - // sandbox - // .stub(axios, "get") - // .resolves({ data: mockData.txDetailsSLPSendAlt }) - // } - // - // const txid = - // "d94357179775425ebc59c93173bd6dc9854095f090a2eb9dcfe9797398bc8eae" - // - // const data = await slp.Utils.decodeOpReturn(txid) - // //console.log(`data: ${JSON.stringify(data, null, 2)}`) - // - // assert2.hasAnyKeys(data, [ - // "transactionType", - // "tokenType", - // "tokenId", - // "spendData" - // ]) - // assert2.isArray(data.spendData) - // assert2.hasAnyKeys(data.spendData[0], ["quantity", "sentTo", "vout"]) - // }) - // }) - describe("#decodeOpReturn", () => { it("should throw an error for a non-string input", async () => { try { @@ -704,6 +533,62 @@ describe("#SLP Utils", () => { assert2.include(err.message, "amount string size not 8 bytes") } }) + + it("should decode a NFT Parent transaction", async () => { + // Mock the call to the REST API. + sandbox + .stub(axios, "get") + .resolves({ data: mockData.txDetailsSLPNftGenesis }) + + const txid = + "4ef6eb92950a13a69e97c2c02c7967d806aa874c0e2a6b5546a8880f2cd14bc4" + + const data = await slp.Utils.decodeOpReturn(txid) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert2.property(data, "tokenType") + assert2.property(data, "txType") + assert2.property(data, "ticker") + assert2.property(data, "name") + assert2.property(data, "tokenId") + assert2.property(data, "documentUri") + assert2.property(data, "documentHash") + assert2.property(data, "decimals") + assert2.property(data, "mintBatonVout") + assert2.property(data, "qty") + + assert2.equal(data.tokenType, 129) + assert2.equal(data.mintBatonVout, 2) + assert2.equal(data.qty, 1) + }) + + // it("should decode a NFT Child transaction", async () => { + // // Mock the call to the REST API. + // // sandbox + // // .stub(axios, "get") + // // .resolves({ data: mockData.txDetailsSLPNftGenesis }) + // + // const txid = + // "3de3766b10506c9156533f1639979e49d1884521543c13e4af73647df1ed3f76" + // + // const data = await slp.Utils.decodeOpReturn(txid) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + // + // // assert2.property(data, "tokenType") + // // assert2.property(data, "txType") + // // assert2.property(data, "ticker") + // // assert2.property(data, "name") + // // assert2.property(data, "tokenId") + // // assert2.property(data, "documentUri") + // // assert2.property(data, "documentHash") + // // assert2.property(data, "decimals") + // // assert2.property(data, "mintBatonVout") + // // assert2.property(data, "qty") + // // + // // assert2.equal(data.tokenType, 129) + // // assert2.equal(data.mintBatonVout, 2) + // // assert2.equal(data.qty, 1) + // }) }) describe("#isTokenUtxo", () => { @@ -1604,6 +1489,96 @@ describe("#SLP Utils", () => { assert2.equal(data[1].utxoType, "minting-baton") assert2.equal(false, data[2]) }) + + it("should decode a NFT Parent transaction", async () => { + // Define stubbed data. + const slpData = { + tokenType: 129, + txType: "GENESIS", + ticker: "NFTTT", + name: "NFT Test Token", + tokenId: + "4ef6eb92950a13a69e97c2c02c7967d806aa874c0e2a6b5546a8880f2cd14bc4", + documentUri: "https://FullStack.cash", + documentHash: "", + decimals: 0, + mintBatonVout: 2, + qty: "1" + } + + const stubValid = [ + { + txid: + "4ef6eb92950a13a69e97c2c02c7967d806aa874c0e2a6b5546a8880f2cd14bc4", + valid: true + } + ] + + // Mock external dependencies. + // Stub the calls to decodeOpReturn. + sandbox + .stub(slp.Utils, "decodeOpReturn") + .resolves(slpData) + .onCall(1) + .resolves(slpData) + .onCall(2) + .resolves(slpData) + .onCall(3) + .resolves(slpData) + .onCall(4) + .resolves(slpData) + + // Stub the call to validateTxid + sandbox + .stub(slp.Utils, "validateTxid") + .resolves(stubValid) + .onCall(1) + .resolves(stubValid) + .onCall(2) + .resolves(stubValid) + + const utxos = [ + { + txid: + "4ef6eb92950a13a69e97c2c02c7967d806aa874c0e2a6b5546a8880f2cd14bc4", + vout: 3, + value: "15620", + height: 638207, + confirmations: 3, + satoshis: 15620 + }, + { + txid: + "4ef6eb92950a13a69e97c2c02c7967d806aa874c0e2a6b5546a8880f2cd14bc4", + vout: 2, + value: "546", + height: 638207, + confirmations: 3, + satoshis: 546 + }, + { + txid: + "4ef6eb92950a13a69e97c2c02c7967d806aa874c0e2a6b5546a8880f2cd14bc4", + 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) + }) }) describe("#txDetails", () => {