From e36d99ca66d70f453dbba199db83c01fd6b2e044 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 28 May 2020 10:11:05 -0700 Subject: [PATCH] fix(tokenUtxoDetails2): Added with unit and integration tests. Uses new decodeOpReturn2() --- src/slp/utils.js | 196 +++++++++++++++++ test/integration/slp.js | 226 +++++++++++++++++-- test/unit/slp-utils.js | 473 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 881 insertions(+), 14 deletions(-) diff --git a/src/slp/utils.js b/src/slp/utils.js index b4953e2..ccd7bbc 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1443,6 +1443,202 @@ class Utils { throw error } } + + async tokenUtxoDetails2(utxos) { + try { + // Throw error if input is not an array. + if (!Array.isArray(utxos)) throw new Error(`Input must be an array.`) + + // Loop through each element in the array and validate the input before + // further processing. + for (let i = 0; i < utxos.length; i++) { + const utxo = utxos[i] + + if (!utxo.satoshis) { + // If Electrumx, convert the value to satoshis. + if (utxo.value) { + utxo.satoshis = utxo.value + } + // If there is neither a satoshis or value property, throw an error. + else { + throw new Error( + `utxo ${i} does not have a satoshis or value property.` + ) + } + } + + if (!utxo.txid) { + // If Electrumx, convert the tx_hash property to txid. + if (utxo.tx_hash) { + utxo.txid = utxo.tx_hash + } + // If there is neither a txid or tx_hash property, throw an error. + else { + throw new Error( + `utxo ${i} does not have a txid or tx_hash property.` + ) + } + } + + if (!Number.isInteger(utxo.vout)) { + if (Number.isInteger(utxo.tx_pos)) { + utxo.vout = utxo.tx_pos + } else { + throw new Error( + `utxo ${i} does not have a vout or tx_pos property.` + ) + } + } + } + + // Output Array + const outAry = [] + + // Loop through each utxo + for (let i = 0; i < utxos.length; i++) { + const utxo = utxos[i] + + // Get raw transaction data from the full node and attempt to decode + // the OP_RETURN data. + // If there is no OP_RETURN, mark the UTXO as false. + let slpData = false + try { + slpData = await this.decodeOpReturn2(utxo.txid) + // 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. + // Mark as false and continue the loop. + outAry.push(false) + continue + } + + const txType = slpData.txType.toLowerCase() + + // console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`) + + // If there is an OP_RETURN, attempt to decode it. + // Handle Genesis SLP transactions. + if (txType === "genesis") { + if ( + utxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output. + utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens. + ) { + // Can safely be marked as false. + outAry[i] = false + } + + // If this is a valid SLP UTXO, then return the decoded OP_RETURN data. + else { + utxo.tokenType = "minting-baton" + utxo.tokenId = utxo.txid + utxo.tokenTicker = slpData.ticker + utxo.tokenName = slpData.name + utxo.tokenDocumentUrl = slpData.documentUri + utxo.tokenDocumentHash = slpData.documentHash + utxo.decimals = slpData.decimals + + // something + outAry[i] = utxo + } + } + + // Handle Mint SLP transactions. + if (txType === "mint") { + if ( + utxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output. + utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens. + ) { + // Can safely be marked as false. + outAry[i] = false + } + + // If UTXO passes validation, then return formatted token data. + else { + const genesisData = await this.decodeOpReturn2(slpData.tokenId) + // console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`) + + // Hydrate the UTXO object with information about the SLP token. + utxo.utxoType = "token" + utxo.transactionType = "mint" + utxo.tokenId = slpData.tokenId + + utxo.tokenTicker = genesisData.ticker + utxo.tokenName = genesisData.name + utxo.tokenDocumentUrl = genesisData.documentUri + utxo.tokenDocumentHash = genesisData.documentHash + utxo.decimals = genesisData.decimals + + utxo.mintBatonVout = slpData.mintBatonVout + + // Calculate the real token quantity. + utxo.tokenQty = + Number(slpData.qty) / Math.pow(10, genesisData.decimals) + + outAry[i] = utxo + } + } + + // Handle Send SLP transactions. + if (txType === "send") { + // Filter out any vouts that match. + // const voutMatch = slpData.spendData.filter(x => utxo.vout === x.vout) + // console.log(`voutMatch: ${JSON.stringify(voutMatch, null, 2)}`) + + // If there are no vout matches, the UTXO can safely be marked as false. + // if (voutMatch.length === 0) { + // outAry[i] = false + // } + + // Figure out what token quantity is represented by this utxo. + const tokenQty = slpData.amounts[utxo.vout - 1] + // console.log(`tokenQty: `, tokenQty) + + if (!tokenQty) { + outAry[i] = false + } + + // If UTXO passes validation, then return formatted token data. + else { + const genesisData = await this.decodeOpReturn2(slpData.tokenId) + // console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`) + + // console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`) + + // Hydrate the UTXO object with information about the SLP token. + utxo.utxoType = "token" + utxo.transactionType = "send" + utxo.tokenId = slpData.tokenId + utxo.tokenTicker = genesisData.ticker + utxo.tokenName = genesisData.name + utxo.tokenDocumentUrl = genesisData.documentUri + utxo.tokenDocumentHash = genesisData.documentHash + utxo.decimals = genesisData.decimals + + // Calculate the real token quantity. + utxo.tokenQty = tokenQty / Math.pow(10, genesisData.decimals) + + // console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`) + + outAry[i] = utxo + } + } + + // 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)}`) + + outAry[i].isValid = isValid[0].valid + } + } + + return outAry + } catch (error) { + if (error.response && error.response.data) throw error.response.data + throw error + } + } } module.exports = Utils diff --git a/test/integration/slp.js b/test/integration/slp.js index ec2e985..af7567a 100644 --- a/test/integration/slp.js +++ b/test/integration/slp.js @@ -635,6 +635,218 @@ describe(`#SLP`, () => { }) }) + describe("#tokenUtxoDetails2", () => { + // // This captures an important corner-case. When an SLP token is created, the + // // change UTXO will contain the same SLP txid, but it is not an SLP UTXO. + it("should return details on minting baton from genesis transaction", async () => { + const utxos = [ + { + txid: + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + vout: 3, + amount: 0.00002015, + satoshis: 2015, + height: 594892, + confirmations: 5 + }, + { + txid: + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + vout: 2, + amount: 0.00000546, + satoshis: 546, + height: 594892, + confirmations: 5 + } + ] + + const data = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.equal(data[0], false, "Change UTXO marked as false.") + + assert.property(data[1], "txid") + assert.property(data[1], "vout") + assert.property(data[1], "amount") + assert.property(data[1], "satoshis") + assert.property(data[1], "height") + assert.property(data[1], "confirmations") + assert.property(data[1], "tokenType") + assert.property(data[1], "tokenId") + assert.property(data[1], "tokenTicker") + assert.property(data[1], "tokenName") + assert.property(data[1], "tokenDocumentUrl") + assert.property(data[1], "tokenDocumentHash") + assert.property(data[1], "decimals") + assert.property(data[1], "isValid") + assert.equal(data[1].isValid, true) + }) + + it("should return details for a MINT token utxo", async () => { + // Mock the call to REST API + + const utxos = [ + { + txid: + "cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76", + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 600297, + confirmations: 76 + } + ] + + const data = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.property(data[0], "txid") + assert.property(data[0], "vout") + assert.property(data[0], "amount") + assert.property(data[0], "satoshis") + assert.property(data[0], "height") + assert.property(data[0], "confirmations") + assert.property(data[0], "utxoType") + assert.property(data[0], "transactionType") + assert.property(data[0], "tokenId") + assert.property(data[0], "tokenTicker") + assert.property(data[0], "tokenName") + assert.property(data[0], "tokenDocumentUrl") + assert.property(data[0], "tokenDocumentHash") + assert.property(data[0], "decimals") + assert.property(data[0], "mintBatonVout") + assert.property(data[0], "tokenQty") + assert.property(data[0], "isValid") + assert.equal(data[0].isValid, true) + }) + + it("should return details for a simple SEND SLP token utxo", async () => { + const utxos = [ + { + txid: + "fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb", + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 596089, + confirmations: 748 + } + ] + + const data = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.property(data[0], "txid") + assert.property(data[0], "vout") + assert.property(data[0], "amount") + assert.property(data[0], "satoshis") + assert.property(data[0], "height") + assert.property(data[0], "confirmations") + assert.property(data[0], "utxoType") + assert.property(data[0], "tokenId") + assert.property(data[0], "tokenTicker") + assert.property(data[0], "tokenName") + assert.property(data[0], "tokenDocumentUrl") + assert.property(data[0], "tokenDocumentHash") + assert.property(data[0], "decimals") + assert.property(data[0], "tokenQty") + assert.property(data[0], "isValid") + assert.equal(data[0].isValid, true) + }) + + it("should handle BCH and SLP utxos in the same TX", async () => { + const utxos = [ + { + txid: + "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56", + vout: 3, + value: "6816", + height: 606848, + confirmations: 13, + satoshis: 6816 + }, + { + txid: + "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56", + vout: 2, + value: "546", + height: 606848, + confirmations: 13, + satoshis: 546 + } + ] + + const result = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 2) + assert.equal(result[0], false) + assert.equal(result[1].isValid, true) + }) + + it("should handle problematic utxos", async () => { + const utxos = [ + { + txid: + "0e3a217fc22612002031d317b4cecd9b692b66b52951a67b23c43041aefa3959", + vout: 0, + amount: 0.00018362, + satoshis: 18362, + height: 613483, + confirmations: 124 + }, + { + txid: + "67fd3c7c3a6eb0fea9ab311b91039545086220f7eeeefa367fa28e6e43009f19", + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 612075, + confirmations: 1532 + } + ] + + const result = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 2) + assert.equal(result[0], false) + assert.equal(result[1].isValid, true) + }) + + it("should return false for BCH-only UTXOs", async () => { + const utxos = [ + { + txid: + "a937f792c7c9eb23b4f344ce5c233d1ac0909217d0a504d71e6b1e4efb864a3b", + vout: 0, + amount: 0.00001, + satoshis: 1000, + confirmations: 0, + ts: 1578424704 + }, + { + txid: + "53fd141c2e999e080a5860887441a2c45e9cbe262027e2bd2ac998fc76e43c44", + vout: 0, + amount: 0.00001, + satoshis: 1000, + confirmations: 0, + ts: 1578424634 + } + ] + + const data = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.isArray(data) + assert.equal(false, data[0]) + assert.equal(false, data[1]) + }) + }) + describe("#balancesForAddress", () => { it(`should throw an error if input is not a string or array of strings`, async () => { try { @@ -653,13 +865,6 @@ describe(`#SLP`, () => { }) it(`should fetch all balances for address: simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9`, async () => { - // Mock the call to rest.bitcoin.com - if (process.env.TEST === "unit") { - sandbox - .stub(axios, "get") - .resolves({ data: mockData.balancesForAddress }) - } - const balances = await bchjs.SLP.Utils.balancesForAddress( "simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9" ) @@ -681,13 +886,6 @@ describe(`#SLP`, () => { "simpleledger:qqss4zp80hn6szsa4jg2s9fupe7g5tcg5ucdyl3r57" ] - // Mock the call to rest.bitcoin.com - if (process.env.TEST === "unit") { - sandbox - .stub(axios, "post") - .resolves({ data: mockData.balancesForAddresses }) - } - const balances = await bchjs.SLP.Utils.balancesForAddress(addresses) // console.log(`balances: ${JSON.stringify(balances, null, 2)}`) diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index f916726..a5ad39a 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -1521,6 +1521,479 @@ describe("#SLP Utils", () => { }) }) + describe("#tokenUtxoDetails2", () => { + it("should throw error if input is not an array.", async () => { + try { + await slp.Utils.tokenUtxoDetails2("test") + + assert2.equal(true, false, "Unexpected result.") + } catch (err) { + assert2.include( + err.message, + `Input must be an array`, + "Expected error message." + ) + } + }) + + it("should throw error if utxo does not have satoshis or value property.", async () => { + try { + const utxos = [ + { + txid: + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + vout: 3, + amount: 0.00002015, + satoshis: 2015, + height: 594892, + confirmations: 5 + }, + { + txid: + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + vout: 2, + amount: 0.00000546, + height: 594892, + confirmations: 5 + } + ] + + await slp.Utils.tokenUtxoDetails2(utxos) + + assert2.equal(true, false, "Unexpected result.") + } catch (err) { + assert2.include( + err.message, + `utxo 1 does not have a satoshis or value property`, + "Expected error message." + ) + } + }) + + it("should throw error if utxo does not have txid or tx_hash property.", async () => { + try { + const utxos = [ + { + txid: + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + vout: 3, + amount: 0.00002015, + satoshis: 2015, + height: 594892, + confirmations: 5 + }, + { + vout: 2, + amount: 0.00000546, + satoshis: 546, + height: 594892, + confirmations: 5 + } + ] + + await slp.Utils.tokenUtxoDetails2(utxos) + + assert2.equal(true, false, "Unexpected result.") + } catch (err) { + assert2.include( + err.message, + `utxo 1 does not have a txid or tx_hash property`, + "Expected error message." + ) + } + }) + + // // This captures an important corner-case. When an SLP token is created, the + // // change UTXO will contain the same SLP txid, but it is not an SLP UTXO. + it("should return details on minting baton from genesis transaction", async () => { + // Mock the call to REST API + // Stub the call to validateTxid + sandbox.stub(slp.Utils, "validateTxid").resolves([ + { + txid: + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + valid: true + } + ]) + + // Stub the calls to decodeOpReturn. + sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({ + tokenType: 1, + txType: "GENESIS", + ticker: "SLPSDK", + name: "SLP SDK example using BITBOX", + tokenId: + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + documentUri: "developer.bitcoin.com", + documentHash: "", + decimals: 8, + mintBatonVout: 2, + qty: "50700000000" + }) + + const utxos = [ + { + txid: + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + vout: 3, + amount: 0.00002015, + satoshis: 2015, + height: 594892, + confirmations: 5 + }, + { + txid: + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + vout: 2, + amount: 0.00000546, + satoshis: 546, + height: 594892, + confirmations: 5 + } + ] + + const data = await slp.Utils.tokenUtxoDetails2(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert2.equal(data[0], false, "Change UTXO marked as false.") + + assert2.property(data[1], "txid") + assert2.property(data[1], "vout") + assert2.property(data[1], "amount") + assert2.property(data[1], "satoshis") + assert2.property(data[1], "height") + assert2.property(data[1], "confirmations") + assert2.property(data[1], "tokenType") + assert2.property(data[1], "tokenId") + assert2.property(data[1], "tokenTicker") + assert2.property(data[1], "tokenName") + assert2.property(data[1], "tokenDocumentUrl") + assert2.property(data[1], "tokenDocumentHash") + assert2.property(data[1], "decimals") + assert2.property(data[1], "isValid") + assert2.equal(data[1].isValid, true) + }) + + it("should return details for a MINT token utxo", async () => { + // Mock the call to REST API + + // Stub the calls to decodeOpReturn. + sandbox + .stub(slp.Utils, "decodeOpReturn2") + .onCall(0) + .resolves({ + tokenType: 1, + txType: "MINT", + tokenId: + "38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0", + mintBatonVout: 2, + qty: "1000000000000" + }) + .onCall(1) + .resolves({ + tokenType: 1, + txType: "GENESIS", + ticker: "PSF", + name: "Permissionless Software Foundation", + tokenId: + "38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0", + documentUri: "psfoundation.cash", + documentHash: "", + decimals: 8, + mintBatonVout: 2, + qty: "1988209163133" + }) + + // Stub the call to validateTxid + sandbox.stub(slp.Utils, "validateTxid").resolves([ + { + txid: + "cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76", + valid: true + } + ]) + + const utxos = [ + { + txid: + "cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76", + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 600297, + confirmations: 76 + } + ] + + const data = await slp.Utils.tokenUtxoDetails2(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert2.property(data[0], "txid") + assert2.property(data[0], "vout") + assert2.property(data[0], "amount") + assert2.property(data[0], "satoshis") + assert2.property(data[0], "height") + assert2.property(data[0], "confirmations") + assert2.property(data[0], "utxoType") + assert2.property(data[0], "transactionType") + assert2.property(data[0], "tokenId") + assert2.property(data[0], "tokenTicker") + assert2.property(data[0], "tokenName") + assert2.property(data[0], "tokenDocumentUrl") + assert2.property(data[0], "tokenDocumentHash") + assert2.property(data[0], "decimals") + assert2.property(data[0], "mintBatonVout") + assert2.property(data[0], "tokenQty") + assert2.property(data[0], "isValid") + assert.equal(data[0].isValid, true) + }) + + it("should return details for a simple SEND SLP token utxo", async () => { + // Mock the call to REST API + // Stub the calls to decodeOpReturn. + sandbox + .stub(slp.Utils, "decodeOpReturn2") + .onCall(0) + .resolves({ + tokenType: 1, + txType: "SEND", + tokenId: + "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7", + amounts: ["200000000", "99887500000000"] + }) + .onCall(1) + .resolves({ + tokenType: 1, + txType: "GENESIS", + ticker: "TOK-CH", + name: "TokyoCash", + tokenId: + "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7", + documentUri: "", + documentHash: "", + decimals: 8, + mintBatonVout: 0, + qty: "2100000000000000" + }) + + // Stub the call to validateTxid + sandbox.stub(slp.Utils, "validateTxid").resolves([ + { + txid: + "fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb", + valid: true + } + ]) + + const utxos = [ + { + txid: + "fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb", + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 596089, + confirmations: 748 + } + ] + + const data = await slp.Utils.tokenUtxoDetails2(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert2.property(data[0], "txid") + assert2.property(data[0], "vout") + assert2.property(data[0], "amount") + assert2.property(data[0], "satoshis") + assert2.property(data[0], "height") + assert2.property(data[0], "confirmations") + assert2.property(data[0], "utxoType") + assert2.property(data[0], "tokenId") + assert2.property(data[0], "tokenTicker") + assert2.property(data[0], "tokenName") + assert2.property(data[0], "tokenDocumentUrl") + assert2.property(data[0], "tokenDocumentHash") + assert2.property(data[0], "decimals") + assert2.property(data[0], "tokenQty") + assert2.property(data[0], "isValid") + assert.equal(data[0].isValid, true) + }) + + it("should handle BCH and SLP utxos in the same TX", async () => { + // Mock external dependencies. + sandbox + .stub(slp.Utils, "validateTxid") + .resolves(mockData.mockDualValidation) + + sandbox + .stub(slp.Utils, "decodeOpReturn2") + .onCall(0) + .resolves({ + tokenType: 1, + txType: "SEND", + tokenId: + "dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d", + amounts: ["1", "5"] + }) + .onCall(1) + .resolves({ + tokenType: 1, + txType: "SEND", + tokenId: + "dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d", + amounts: ["1", "5"] + }) + .onCall(2) + .resolves({ + tokenType: 1, + txType: "GENESIS", + ticker: "TAP", + name: "Thoughts and Prayers", + tokenId: + "dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d", + documentUri: "", + documentHash: "", + decimals: 0, + mintBatonVout: 2, + qty: "1000000" + }) + + const utxos = [ + { + txid: + "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56", + vout: 3, + value: "6816", + height: 606848, + confirmations: 13, + satoshis: 6816 + }, + { + txid: + "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56", + vout: 2, + value: "546", + height: 606848, + confirmations: 13, + satoshis: 546 + } + ] + + const result = await slp.Utils.tokenUtxoDetails2(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert2.isArray(result) + assert2.equal(result.length, 2) + assert2.equal(result[0], false) + assert.equal(result[1].isValid, true) + }) + + it("should handle problematic utxos", async () => { + // Mock external dependencies. + // Stub the calls to decodeOpReturn. + sandbox + .stub(slp.Utils, "decodeOpReturn2") + .onCall(0) + .throws({ message: "scriptpubkey not op_return" }) + .onCall(1) + .resolves({ + tokenType: 1, + txType: "SEND", + tokenId: + "f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f", + amounts: ["5000000", "395010942"] + }) + .onCall(2) + .resolves({ + tokenType: 1, + txType: "GENESIS", + ticker: "AUDC", + name: "AUD Coin", + tokenId: + "f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f", + documentUri: "audcoino@gmail.com", + documentHash: "", + decimals: 6, + mintBatonVout: 0, + qty: "2000000000000000000" + }) + + // Stub the call to validateTxid + sandbox.stub(slp.Utils, "validateTxid").resolves([ + { + txid: + "67fd3c7c3a6eb0fea9ab311b91039545086220f7eeeefa367fa28e6e43009f19", + valid: true + } + ]) + + const utxos = [ + { + txid: + "0e3a217fc22612002031d317b4cecd9b692b66b52951a67b23c43041aefa3959", + vout: 0, + amount: 0.00018362, + satoshis: 18362, + height: 613483, + confirmations: 124 + }, + { + txid: + "67fd3c7c3a6eb0fea9ab311b91039545086220f7eeeefa367fa28e6e43009f19", + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 612075, + confirmations: 1532 + } + ] + + const result = await slp.Utils.tokenUtxoDetails2(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert2.isArray(result) + assert2.equal(result.length, 2) + assert2.equal(result[0], false) + assert2.equal(result[1].isValid, true) + }) + + it("should return false for BCH-only UTXOs", async () => { + // Mock live network calls + if (process.env.TEST === "unit") { + sandbox + .stub(slp.Utils, "decodeOpReturn2") + .throws(new Error("scriptpubkey not op_return")) + } + + const utxos = [ + { + txid: + "a937f792c7c9eb23b4f344ce5c233d1ac0909217d0a504d71e6b1e4efb864a3b", + vout: 0, + amount: 0.00001, + satoshis: 1000, + confirmations: 0, + ts: 1578424704 + }, + { + txid: + "53fd141c2e999e080a5860887441a2c45e9cbe262027e2bd2ac998fc76e43c44", + vout: 0, + amount: 0.00001, + satoshis: 1000, + confirmations: 0, + ts: 1578424634 + } + ] + + const data = await slp.Utils.tokenUtxoDetails2(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert2.isArray(data) + assert2.equal(false, data[0]) + assert2.equal(false, data[1]) + }) + }) + describe("#txDetails", () => { it("should throw an error if txid is not included", async () => { try {