From de5b5deace9b00ca7fc15b2bbc0459f3242e4d91 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 26 May 2020 15:02:46 -0700 Subject: [PATCH 1/5] fix(rate-limit e2e): Updating e2e tests for rate limits --- test/e2e/rate-limits/anonymous-rate-limits.js | 2 +- test/e2e/rate-limits/free-rate-limits.js | 47 +++++++++++++++---- test/e2e/rate-limits/indexer-rate-limits.js | 4 +- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/test/e2e/rate-limits/anonymous-rate-limits.js b/test/e2e/rate-limits/anonymous-rate-limits.js index 6bee35c..50643c7 100644 --- a/test/e2e/rate-limits/anonymous-rate-limits.js +++ b/test/e2e/rate-limits/anonymous-rate-limits.js @@ -8,7 +8,7 @@ const assert = require("chai").assert -const RESTURL = `https://api.bchjs.cash/v3/` +const RESTURL = `https://api.fullstack.cash/v3/` // const RESTURL = `http://localhost:3000/v3/` const BCHJS = require("../../../src/bch-js") diff --git a/test/e2e/rate-limits/free-rate-limits.js b/test/e2e/rate-limits/free-rate-limits.js index b1301f0..4a4b4dd 100644 --- a/test/e2e/rate-limits/free-rate-limits.js +++ b/test/e2e/rate-limits/free-rate-limits.js @@ -9,13 +9,12 @@ const assert = require("chai").assert -// const JWT_TOKEN = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYmM5ZTI3YWIwMDI4M2E1MDhkYzQ4OSIsImlhdCI6MTU3MjY0MjM1NCwiZXhwIjoxNTc1MjM0MzU0fQ.6dzUh10UXoQXjJujRZ0AMSBhz0ElM1Cc-rCyb50PDqI` const JWT_TOKEN = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYmM5ZTI3YWIwMDI4M2E1MDhkYzQ4OSIsImlhdCI6MTU3MjgzMjAzNywiZXhwIjoxNTc1NDI0MDM3fQ.FygzeHYGH5vsXurFlI7ZFTmS2eyLDt3iglhFWgIZRxY" + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2YyMDIyMWMxMDAxMmFkOTQwMSIsImVtYWlsIjoiZGVtb0BkZW1vLmNvbSIsImFwaUxldmVsIjoxMCwicmF0ZUxpbWl0IjozLCJpYXQiOjE1OTA1Mjk2NzgsImV4cCI6MTU5MzEyMTY3OH0.4q5MFDCcVx0FusivJPiEIj3w8nhFG7ZV5IhJS7faUlc" const BCHJS = require("../../../src/bch-js") const bchjs = new BCHJS({ - restURL: `https://api.bchjs.cash/v3/`, + restURL: `https://api.fullstack.cash/v3/`, // restURL: `http://localhost:3000/v3/`, apiToken: JWT_TOKEN }) @@ -29,18 +28,42 @@ describe("#free rate limits", () => { assert.property(result, "balance") }).timeout(5000) - it("should throw error when rate limit exceeded 3 RPM for indexer endpoints", async () => { - const addr = "bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" + // it("should throw error when rate limit exceeded 3 RPM for indexer endpoints", async () => { + // const addr = "bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" + // + // try { + // for (let i = 0; i < 5; i++) await bchjs.Blockbook.balance(addr) + // + // assert.equal(true, false, "unexpected result") + // } catch (err) { + // // console.log(`err: `, err) + // assert.include(err.error, "Too many requests") + // } + // }).timeout(10000) + it("should allow up to 10 RPM to indexer, then throw error", async () => { try { - for (let i = 0; i < 5; i++) await bchjs.Blockbook.balance(addr) + const addr = "bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0" - assert.equal(true, false, "unexpected result") + for (let i = 0; i < 15; i++) { + const result = await await bchjs.Blockbook.balance(addr) + + if (i === 5) { + // console.log(`validating 5th call: ${i}`) + assert.property(result, "address", "more than 3 calls allowed") + } + } } catch (err) { // console.log(`err: `, err) - assert.include(err.error, "Too many requests") + assert.include( + err.error, + "currently 10 requests", + "more than 10 not allowed" + ) + assert.include(err.error, 10) + assert.notInclude(err.error, 3) } - }).timeout(10000) + }).timeout(20000) it("should allow up to 10 RPM to full node, then throw error", async () => { try { @@ -55,7 +78,11 @@ describe("#free rate limits", () => { } catch (err) { // console.log(`validating after 10th call`) // console.log(`err: `, err) - assert.include(err.error, "Too many requests", "more than 10 not allowed") + assert.include( + err.error, + "currently 10 requests", + "more than 10 not allowed" + ) assert.include(err.error, 10) assert.notInclude(err.error, 3) } diff --git a/test/e2e/rate-limits/indexer-rate-limits.js b/test/e2e/rate-limits/indexer-rate-limits.js index 09d5be1..8207f29 100644 --- a/test/e2e/rate-limits/indexer-rate-limits.js +++ b/test/e2e/rate-limits/indexer-rate-limits.js @@ -10,11 +10,11 @@ const assert = require("chai").assert const JWT_TOKEN = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYmY4MjA1YTYwODliMjliYTlhZjc1OSIsImlhdCI6MTU3MjgzMjEwNywiZXhwIjoxNTc1NDI0MTA3fQ.UD-36TKwN65-zePwNzujGmHeQ60fKqYPunawGxktwws" + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTU5MDQzMjIxNywiZXhwIjoxNTkzMDI0MjE3fQ.R0Euhutd5FHCxLlSzjq1TYsGjI6iua_AguWIeLK2xCU" const BCHJS = require("../../../src/bch-js") const bchjs = new BCHJS({ - restURL: `https://api.bchjs.cash/v3/`, + restURL: `https://api.fullstack.cash/v3/`, // restURL: `http://localhost:3000/v3/`, apiToken: JWT_TOKEN }) From e5cd03208ad729a27143edebe12f3a3e58ce52b6 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 26 May 2020 19:39:26 -0700 Subject: [PATCH 2/5] Refactored SLP.Utils.isTokenUtxo(). Needs updated tests --- src/slp/utils.js | 164 +++++--- test/integration/slp.js | 867 +++++++++++++++++++++------------------- test/unit/slp-utils.js | 4 +- 3 files changed, 572 insertions(+), 463 deletions(-) diff --git a/src/slp/utils.js b/src/slp/utils.js index 5a35449..c47fea8 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1053,11 +1053,10 @@ class Utils { * Expects an array of UTXO objects as input. Returns an array of Boolean * values indicating if a UTXO is associated with SLP tokens (true) or not * (false). - * Note: There is no way to validate SLP UTXOs without inspecting the OP_RETURN. - * If a UTXO returns false when its txid is passed to validateTxid(), then the - * UTXO is not associated with SLP. This is a fast and quick check. - * If a UTXO returns true though, the OP_RETURN has to be inspected to determine - * for sure that it *is* associated with an SLP transaction not just change. + * Expects an array of UTXO objects as input. Returns the same array with + * additional properties for each UTXO, including an isSlp property which + * will be true or false indicating if the UTXO belongs to a valid SLP + * transaction. * * @apiExample Example usage: * @@ -1082,6 +1081,12 @@ class Utils { * burnTotal: 100 * } */ + + // Note: There is no way to validate SLP UTXOs without inspecting the OP_RETURN. + // If a UTXO returns false when its txid is passed to validateTxid(), then the + // UTXO is not associated with SLP. This is a fast and quick check. + // If a UTXO returns true though, the OP_RETURN has to be inspected to determine + // for sure that it *is* associated with an SLP transaction not just change. async isTokenUtxo(utxos) { try { // Throw error if input is not an array. @@ -1092,59 +1097,108 @@ class Utils { for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i] - // Throw error if utxo does not have a satoshis property. - if (!utxo.satoshis) - throw new Error(`utxo ${i} does not have a satoshis property.`) - - // Throw error if utxo does not have a txid property. - if (!utxo.txid) - throw new Error(`utxo ${i} does not have a txid property.`) - } - - // Create an array of txid strings to feed to validateTxid - const txids = utxos.map(x => x.txid) - - // Validate the array of txids. - let validations = await this.validateTxid(txids) - //console.log(`validations: ${JSON.stringify(validations, null, 2)}`) - - // Extract the boolean result - validations = validations.map(x => x.valid) - - // Loop through each element in the array and validate the input before - // further processing. - for (let i = 0; i < utxos.length; i++) { - const thisValidation = validations[i] - const utxo = utxos[i] - - // Only need to worry about validations that are still true. - if (thisValidation) { - const slpData = await this.decodeOpReturn(utxo.txid) - //console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`) - - // Handle Genesis and Mint SLP transactions. - if ( - slpData.transactionType === "genesis" || - slpData.transactionType === "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. + 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.` ) - // Can safely be marked as false. - validations[i] = false - } else if (slpData.transactionType === "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) validations[i] = false } } + 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 (!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.` + ) + } + } + } + + const validatedSlpUtxos = [] + + // Loop through each utxo. + for (let i = 0; i < utxos.length; i++) { + const utxo = utxos[i] + + // console.log(`thisValidation: `, thisValidation) + // console.log(`utxo: `, utxo) + + // Validate the txid for the UTXO using SLPDB. + const isValid = await _this.validateTxid(utxo.txid) + + // If SLPDB says the UTXOs TXID is invalid, mark the UTXO and move on. + if (!isValid) { + utxo.isSlp = false + validatedSlpUtxos.push(utxo) + continue + } + + let slpData = {} + try { + // Decode the OP_RETURN. + slpData = await _this.decodeOpReturn2(utxo.txid) + console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`) + } catch (err) { + // console.log(`decodeOpReturn2 error: `, err) + // If that function throws an error, then mark the UTXO as false. + utxo.isSlp = false + validatedSlpUtxos.push(utxo) + continue + } + + const txType = slpData.txType.toLowerCase() + + // Handle Genesis and Mint SLP transactions. + if (txType === "genesis" || 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. + utxo.isSlp = false + validatedSlpUtxos.push(utxo) + continue + } + // + + // Handle Send transactions. + } else if (txType === "send") { + const slpOutputs = slpData.amounts.length + + // If the UTXO vout value does not line up with an SLP output in the + // OP_RETURN, then it is not an SLP UTXO. + if (utxo.vout > slpOutputs) { + utxo.isSlp = false + validatedSlpUtxos.push(utxo) + continue + } + } + + // If code path gets to this point and it hasn't been marked false, + // then it can safely be marked as true. + utxo.isSlp = true + validatedSlpUtxos.push(utxo) + // Below is deprecated, but kept for posterity: // Invalidate the utxo if it contains more than dust, since SLP token // UTXOs only contain dust values. <--- NOT TRUE @@ -1153,7 +1207,7 @@ class Utils { //if (utxo.satoshis > 546) validations[i] = false } - return validations + return validatedSlpUtxos } catch (error) { if (error.response && error.response.data) throw error.response.data throw error diff --git a/test/integration/slp.js b/test/integration/slp.js index 9f3a179..19531f0 100644 --- a/test/integration/slp.js +++ b/test/integration/slp.js @@ -28,45 +28,45 @@ describe(`#SLP`, () => { }) describe("#util", () => { - describe("#list", () => { - it(`should get information on the Spice token`, async () => { - const tokenId = `4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf` - - const result = await bchjs.SLP.Utils.list(tokenId) - //console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.hasAnyKeys(result, [ - "decimals", - "timestamp", - "timestamp_unix", - "versionType", - "documentUri", - "symbol", - "name", - "containsBaton", - "id", - "documentHash", - "initialTokenQty", - "blockCreated", - "blockLastActiveSend", - "blockLastActiveMint", - "txnsSinceGenesis", - "validAddress", - "totalMinted", - "totalBurned", - "circulatingSupply", - "mintingBatonStatus" - ]) - }) - }) - + // describe("#list", () => { + // it(`should get information on the Spice token`, async () => { + // const tokenId = `4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf` + // + // const result = await bchjs.SLP.Utils.list(tokenId) + // //console.log(`result: ${JSON.stringify(result, null, 2)}`) + // + // assert.hasAnyKeys(result, [ + // "decimals", + // "timestamp", + // "timestamp_unix", + // "versionType", + // "documentUri", + // "symbol", + // "name", + // "containsBaton", + // "id", + // "documentHash", + // "initialTokenQty", + // "blockCreated", + // "blockLastActiveSend", + // "blockLastActiveMint", + // "txnsSinceGenesis", + // "validAddress", + // "totalMinted", + // "totalBurned", + // "circulatingSupply", + // "mintingBatonStatus" + // ]) + // }) + // }) + // describe("#decodeOpReturn", () => { it("should decode the OP_RETURN for a SEND txid", async () => { const txid = "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1" const result = await bchjs.SLP.Utils.decodeOpReturn(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) + console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.hasAllKeys(result, [ "tokenType", @@ -138,7 +138,7 @@ describe(`#SLP`, () => { "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1" const result = await bchjs.SLP.Utils.decodeOpReturn2(txid) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) + console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.hasAllKeys(result, ["tokenType", "txType", "tokenId", "amounts"]) assert.equal( @@ -227,386 +227,441 @@ describe(`#SLP`, () => { }) }) - describe("#tokenUtxoDetails", () => { - it("should return token details on a valid UTXO", async () => { - const utxos = [ - { - txid: - "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1", - vout: 1, - value: "546", - height: 597740, - confirmations: 1, - satoshis: 546 - } - ] + describe("#isTokenUtxo", () => { + // it("should accurately analyze UTXOs from Blockbook", async () => { + // const addr = "bitcoincash:qqkpnx2rff4q5jranf3wk9jsprhae9unxckq07gph4" + // + // // const utxos = await bchjs.Blockbook.utxo(addr) + // // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) + // const utxos = [ + // { + // txid: + // "99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730", + // vout: 1, + // value: "546", + // height: 636885, + // confirmations: 3, + // satoshis: 546 + // }, + // { + // txid: + // "2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7", + // vout: 0, + // value: "600", + // height: 636885, + // confirmations: 3, + // satoshis: 600 + // } + // ] + // + // const isTokenUtxos = await bchjs.SLP.Utils.isTokenUtxo(utxos) + // // console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`) + // + // assert.isArray(isTokenUtxos) + // assert.equal(isTokenUtxos[0], true) + // assert.equal(isTokenUtxos[1], false) + // }) - const result = await bchjs.SLP.Utils.tokenUtxoDetails(utxos) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.hasAllKeys(result[0], [ - "txid", - "vout", - "value", - "height", - "confirmations", - "satoshis", - "utxoType", - "transactionType", - "tokenId", - "tokenTicker", - "tokenName", - "tokenDocumentUrl", - "tokenDocumentHash", - "decimals", - "tokenQty", - "isValid" - ]) - }) - - it("should not choke on this problematic utxo", async () => { - const utxos = [ - { - txid: - "a8eb788b8ddda6faea00e6e2756624b8feb97655363d0400dd66839ea619d36e", - vout: 1, - value: "546", - height: 603282, - confirmations: 156, - satoshis: 546 - } - ] - - const result = await bchjs.SLP.Utils.tokenUtxoDetails(utxos) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.hasAllKeys(result[0], [ - "txid", - "vout", - "value", - "height", - "confirmations", - "satoshis", - "utxoType", - "transactionType", - "tokenId", - "tokenTicker", - "tokenName", - "tokenDocumentUrl", - "tokenDocumentHash", - "decimals", - "tokenQty", - "isValid" - ]) - }) - - 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.tokenUtxoDetails(utxos) - // console.log(`result: ${JSON.stringify(result, null, 2)}`) - - assert.isArray(result) - assert.equal(result.length, 2) - assert.equal(result[0], false) - }) - - // 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.tokenUtxoDetails(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 () => { - const utxos = [ - { - txid: - "cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76", - vout: 1, - amount: 0.00000546, - satoshis: 546, - height: 600297, - confirmations: 76 - } - ] - - const data = await bchjs.SLP.Utils.tokenUtxoDetails(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], "batonStillExists") - 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.tokenUtxoDetails(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.tokenUtxoDetails(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.tokenUtxoDetails(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.tokenUtxoDetails(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 () => { + it("should accurately analyze UTXOs from Electrumx", async () => { try { - const address = 1234 + const addr = "bitcoincash:qqkpnx2rff4q5jranf3wk9jsprhae9unxckq07gph4" - await bchjs.SLP.Utils.balancesForAddress(address) + const utxos = await bchjs.Electrumx.utxo(addr) + console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) - assert.equal(true, false, "Uh oh. Code path should not end here.") + const isTokenUtxos = await bchjs.SLP.Utils.isTokenUtxo(utxos.utxos) + console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`) + + // assert.isArray(isTokenUtxos) + // assert.equal(isTokenUtxos[0], true) + // assert.equal(isTokenUtxos[1], false) } catch (err) { - //console.log(`Error: `, err) - assert.include( - err.message, - `Input address must be a string or array of strings` - ) + console.log(`Error: `, err) } }) - - 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" - ) - // console.log(`balances: ${JSON.stringify(balances, null, 2)}`) - - assert.isArray(balances) - assert.hasAllKeys(balances[0], [ - "tokenId", - "balanceString", - "balance", - "decimalCount", - "slpAddress" - ]) - }) - - it(`should fetch balances for multiple addresses`, async () => { - const addresses = [ - "simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9", - "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)}`) - - assert.isArray(balances) - assert.isArray(balances[0]) - assert.hasAllKeys(balances[0][0], [ - "tokenId", - "balanceString", - "balance", - "decimalCount", - "slpAddress" - ]) - }) }) + + // describe("#tokenUtxoDetails", () => { + // it("should return token details on a valid UTXO", async () => { + // const utxos = [ + // { + // txid: + // "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1", + // vout: 1, + // value: "546", + // height: 597740, + // confirmations: 1, + // satoshis: 546 + // } + // ] + // + // const result = await bchjs.SLP.Utils.tokenUtxoDetails(utxos) + // // console.log(`result: ${JSON.stringify(result, null, 2)}`) + // + // assert.hasAllKeys(result[0], [ + // "txid", + // "vout", + // "value", + // "height", + // "confirmations", + // "satoshis", + // "utxoType", + // "transactionType", + // "tokenId", + // "tokenTicker", + // "tokenName", + // "tokenDocumentUrl", + // "tokenDocumentHash", + // "decimals", + // "tokenQty", + // "isValid" + // ]) + // }) + // + // it("should not choke on this problematic utxo", async () => { + // const utxos = [ + // { + // txid: + // "a8eb788b8ddda6faea00e6e2756624b8feb97655363d0400dd66839ea619d36e", + // vout: 1, + // value: "546", + // height: 603282, + // confirmations: 156, + // satoshis: 546 + // } + // ] + // + // const result = await bchjs.SLP.Utils.tokenUtxoDetails(utxos) + // // console.log(`result: ${JSON.stringify(result, null, 2)}`) + // + // assert.hasAllKeys(result[0], [ + // "txid", + // "vout", + // "value", + // "height", + // "confirmations", + // "satoshis", + // "utxoType", + // "transactionType", + // "tokenId", + // "tokenTicker", + // "tokenName", + // "tokenDocumentUrl", + // "tokenDocumentHash", + // "decimals", + // "tokenQty", + // "isValid" + // ]) + // }) + // + // 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.tokenUtxoDetails(utxos) + // // console.log(`result: ${JSON.stringify(result, null, 2)}`) + // + // assert.isArray(result) + // assert.equal(result.length, 2) + // assert.equal(result[0], false) + // }) + // + // // 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.tokenUtxoDetails(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 () => { + // const utxos = [ + // { + // txid: + // "cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76", + // vout: 1, + // amount: 0.00000546, + // satoshis: 546, + // height: 600297, + // confirmations: 76 + // } + // ] + // + // const data = await bchjs.SLP.Utils.tokenUtxoDetails(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], "batonStillExists") + // 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.tokenUtxoDetails(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.tokenUtxoDetails(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.tokenUtxoDetails(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.tokenUtxoDetails(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 { + // const address = 1234 + // + // await bchjs.SLP.Utils.balancesForAddress(address) + // + // assert.equal(true, false, "Uh oh. Code path should not end here.") + // } catch (err) { + // //console.log(`Error: `, err) + // assert.include( + // err.message, + // `Input address must be a string or array of strings` + // ) + // } + // }) + // + // 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" + // ) + // // console.log(`balances: ${JSON.stringify(balances, null, 2)}`) + // + // assert.isArray(balances) + // assert.hasAllKeys(balances[0], [ + // "tokenId", + // "balanceString", + // "balance", + // "decimalCount", + // "slpAddress" + // ]) + // }) + // + // it(`should fetch balances for multiple addresses`, async () => { + // const addresses = [ + // "simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9", + // "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)}`) + // + // assert.isArray(balances) + // assert.isArray(balances[0]) + // assert.hasAllKeys(balances[0][0], [ + // "tokenId", + // "balanceString", + // "balance", + // "decimalCount", + // "slpAddress" + // ]) + // }) + // }) + // }) }) diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index 115a407..9c75d85 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -808,7 +808,7 @@ describe("#SLP Utils", () => { ]) // Stub the calls to decodeOpReturn. - sandbox.stub(slp.Utils, "decodeOpReturn").resolves({ + sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({ tokenType: 1, transactionType: "genesis", ticker: "SLPSDK", @@ -870,7 +870,7 @@ describe("#SLP Utils", () => { ]) // Stub the calls to decodeOpReturn. - sandbox.stub(slp.Utils, "decodeOpReturn").resolves({ + sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({ tokenType: 1, transactionType: "send", tokenId: From b528a69c58fa8a5ce9ad83bd219bd3bd56c8468e Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 27 May 2020 11:56:36 -0700 Subject: [PATCH 3/5] feat(SLP Utils): Refactoring SLP Utils lib. Starting with isTokenUtxo() --- src/slp/utils.js | 42 +- test/integration/slp.js | 946 +++++++++++++++++++++------------------- test/unit/slp-utils.js | 262 ++++++++--- 3 files changed, 714 insertions(+), 536 deletions(-) diff --git a/src/slp/utils.js b/src/slp/utils.js index c47fea8..b4953e2 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1050,9 +1050,6 @@ class Utils { * @apiGroup SLP * @apiDescription Determine if UTXO belongs to an SLP transaction. * - * Expects an array of UTXO objects as input. Returns an array of Boolean - * values indicating if a UTXO is associated with SLP tokens (true) or not - * (false). * Expects an array of UTXO objects as input. Returns the same array with * additional properties for each UTXO, including an isSlp property which * will be true or false indicating if the UTXO belongs to a valid SLP @@ -1062,24 +1059,38 @@ class Utils { * * (async () => { * try { - * const u = await bchjs.Insight.utxos(`bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05`) - * const utxos = u.utxos + * const addr = 'bitcoincash:qqkpnx2rff4q5jranf3wk9jsprhae9unxckq07gph4' + * const utxos = await bchjs.Blockbook.utxo(addr) * * const isSLPUtxo = await bchjs.SLP.Utils.isTokenUtxo(utxos) * - * console.log(`isSLPUtxo: ${JSON.stringify(isSLPUtxo,null,2)}`) + * console.log(`${JSON.stringify(isSLPUtxo,null,2)}`) * } catch (error) { * console.error(error) * } * })() * * // returns - * { - * transactionId: 'c7078a6c7400518a513a0bde1f4158cf740d08d3b5bfb19aa7b6657e2f4160de', - * inputTotal: 100000100, - * outputTotal: 100000000, - * burnTotal: 100 - * } + * [ + * { + * "txid": "99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730", + * "vout": 1, + * "value": "546", + * "height": 636885, + * "confirmations": 123, + * "satoshis": 546, + * "isSlp": true + * }, + * { + * "txid": "2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7", + * "vout": 0, + * "value": "600", + * "height": 636885, + * "confirmations": 123, + * "satoshis": 600, + * "isSlp": false + * } + * ] */ // Note: There is no way to validate SLP UTXOs without inspecting the OP_RETURN. @@ -1123,7 +1134,7 @@ class Utils { } } - if (!utxo.vout) { + if (!Number.isInteger(utxo.vout)) { if (Number.isInteger(utxo.tx_pos)) { utxo.vout = utxo.tx_pos } else { @@ -1145,6 +1156,7 @@ class Utils { // Validate the txid for the UTXO using SLPDB. const isValid = await _this.validateTxid(utxo.txid) + // console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`) // If SLPDB says the UTXOs TXID is invalid, mark the UTXO and move on. if (!isValid) { @@ -1153,11 +1165,11 @@ class Utils { continue } + // Decode the OP_RETURN. let slpData = {} try { - // Decode the OP_RETURN. slpData = await _this.decodeOpReturn2(utxo.txid) - console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`) + // console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`) } catch (err) { // console.log(`decodeOpReturn2 error: `, err) // If that function throws an error, then mark the UTXO as false. diff --git a/test/integration/slp.js b/test/integration/slp.js index 19531f0..ec2e985 100644 --- a/test/integration/slp.js +++ b/test/integration/slp.js @@ -28,45 +28,45 @@ describe(`#SLP`, () => { }) describe("#util", () => { - // describe("#list", () => { - // it(`should get information on the Spice token`, async () => { - // const tokenId = `4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf` - // - // const result = await bchjs.SLP.Utils.list(tokenId) - // //console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.hasAnyKeys(result, [ - // "decimals", - // "timestamp", - // "timestamp_unix", - // "versionType", - // "documentUri", - // "symbol", - // "name", - // "containsBaton", - // "id", - // "documentHash", - // "initialTokenQty", - // "blockCreated", - // "blockLastActiveSend", - // "blockLastActiveMint", - // "txnsSinceGenesis", - // "validAddress", - // "totalMinted", - // "totalBurned", - // "circulatingSupply", - // "mintingBatonStatus" - // ]) - // }) - // }) - // + describe("#list", () => { + it(`should get information on the Spice token`, async () => { + const tokenId = `4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf` + + const result = await bchjs.SLP.Utils.list(tokenId) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAnyKeys(result, [ + "decimals", + "timestamp", + "timestamp_unix", + "versionType", + "documentUri", + "symbol", + "name", + "containsBaton", + "id", + "documentHash", + "initialTokenQty", + "blockCreated", + "blockLastActiveSend", + "blockLastActiveMint", + "txnsSinceGenesis", + "validAddress", + "totalMinted", + "totalBurned", + "circulatingSupply", + "mintingBatonStatus" + ]) + }) + }) + describe("#decodeOpReturn", () => { it("should decode the OP_RETURN for a SEND txid", async () => { const txid = "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1" const result = await bchjs.SLP.Utils.decodeOpReturn(txid) - console.log(`result: ${JSON.stringify(result, null, 2)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.hasAllKeys(result, [ "tokenType", @@ -138,7 +138,7 @@ describe(`#SLP`, () => { "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1" const result = await bchjs.SLP.Utils.decodeOpReturn2(txid) - console.log(`result: ${JSON.stringify(result, null, 2)}`) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.hasAllKeys(result, ["tokenType", "txType", "tokenId", "amounts"]) assert.equal( @@ -228,440 +228,480 @@ describe(`#SLP`, () => { }) describe("#isTokenUtxo", () => { - // it("should accurately analyze UTXOs from Blockbook", async () => { - // const addr = "bitcoincash:qqkpnx2rff4q5jranf3wk9jsprhae9unxckq07gph4" - // - // // const utxos = await bchjs.Blockbook.utxo(addr) - // // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) - // const utxos = [ - // { - // txid: - // "99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730", - // vout: 1, - // value: "546", - // height: 636885, - // confirmations: 3, - // satoshis: 546 - // }, - // { - // txid: - // "2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7", - // vout: 0, - // value: "600", - // height: 636885, - // confirmations: 3, - // satoshis: 600 - // } - // ] - // - // const isTokenUtxos = await bchjs.SLP.Utils.isTokenUtxo(utxos) - // // console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`) - // - // assert.isArray(isTokenUtxos) - // assert.equal(isTokenUtxos[0], true) - // assert.equal(isTokenUtxos[1], false) - // }) + it("should accurately analyze UTXOs from Blockbook", async () => { + const addr = "bitcoincash:qqkpnx2rff4q5jranf3wk9jsprhae9unxckq07gph4" + + const utxos = await bchjs.Blockbook.utxo(addr) + // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) + + const isTokenUtxos = await bchjs.SLP.Utils.isTokenUtxo(utxos) + // console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`) + + // Filter out the result that is associated with the known non-slp tx. + const nonSlp = isTokenUtxos.filter( + x => + x.txid === + "2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7" + ) + + const isSlp = isTokenUtxos.filter( + x => + x.txid === + "99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730" + ) + + assert.isArray(isTokenUtxos) + assert.equal(nonSlp[0].isSlp, false) + assert.equal(isSlp[0].isSlp, true) + }) it("should accurately analyze UTXOs from Electrumx", async () => { try { const addr = "bitcoincash:qqkpnx2rff4q5jranf3wk9jsprhae9unxckq07gph4" const utxos = await bchjs.Electrumx.utxo(addr) - console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) + // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) const isTokenUtxos = await bchjs.SLP.Utils.isTokenUtxo(utxos.utxos) - console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`) + // console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`) - // assert.isArray(isTokenUtxos) - // assert.equal(isTokenUtxos[0], true) - // assert.equal(isTokenUtxos[1], false) + // Filter out the result that is associated with the known non-slp tx. + const nonSlp = isTokenUtxos.filter( + x => + x.tx_hash === + "2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7" + ) + + const isSlp = isTokenUtxos.filter( + x => + x.tx_hash === + "99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730" + ) + + assert.isArray(isTokenUtxos) + assert.equal(nonSlp[0].isSlp, false) + assert.equal(isSlp[0].isSlp, true) } catch (err) { console.log(`Error: `, err) } }) + + // 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 false for change in an SLP token creation 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.isTokenUtxo(utxos) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert.equal( + data[0].isSlp, + false, + "Change should not be identified as SLP utxo." + ) + assert.equal(data[1].isSlp, true, "SLP UTXO correctly identified.") + }) }) - // describe("#tokenUtxoDetails", () => { - // it("should return token details on a valid UTXO", async () => { - // const utxos = [ - // { - // txid: - // "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1", - // vout: 1, - // value: "546", - // height: 597740, - // confirmations: 1, - // satoshis: 546 - // } - // ] - // - // const result = await bchjs.SLP.Utils.tokenUtxoDetails(utxos) - // // console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.hasAllKeys(result[0], [ - // "txid", - // "vout", - // "value", - // "height", - // "confirmations", - // "satoshis", - // "utxoType", - // "transactionType", - // "tokenId", - // "tokenTicker", - // "tokenName", - // "tokenDocumentUrl", - // "tokenDocumentHash", - // "decimals", - // "tokenQty", - // "isValid" - // ]) - // }) - // - // it("should not choke on this problematic utxo", async () => { - // const utxos = [ - // { - // txid: - // "a8eb788b8ddda6faea00e6e2756624b8feb97655363d0400dd66839ea619d36e", - // vout: 1, - // value: "546", - // height: 603282, - // confirmations: 156, - // satoshis: 546 - // } - // ] - // - // const result = await bchjs.SLP.Utils.tokenUtxoDetails(utxos) - // // console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.hasAllKeys(result[0], [ - // "txid", - // "vout", - // "value", - // "height", - // "confirmations", - // "satoshis", - // "utxoType", - // "transactionType", - // "tokenId", - // "tokenTicker", - // "tokenName", - // "tokenDocumentUrl", - // "tokenDocumentHash", - // "decimals", - // "tokenQty", - // "isValid" - // ]) - // }) - // - // 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.tokenUtxoDetails(utxos) - // // console.log(`result: ${JSON.stringify(result, null, 2)}`) - // - // assert.isArray(result) - // assert.equal(result.length, 2) - // assert.equal(result[0], false) - // }) - // - // // 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.tokenUtxoDetails(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 () => { - // const utxos = [ - // { - // txid: - // "cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76", - // vout: 1, - // amount: 0.00000546, - // satoshis: 546, - // height: 600297, - // confirmations: 76 - // } - // ] - // - // const data = await bchjs.SLP.Utils.tokenUtxoDetails(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], "batonStillExists") - // 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.tokenUtxoDetails(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.tokenUtxoDetails(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.tokenUtxoDetails(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.tokenUtxoDetails(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 { - // const address = 1234 - // - // await bchjs.SLP.Utils.balancesForAddress(address) - // - // assert.equal(true, false, "Uh oh. Code path should not end here.") - // } catch (err) { - // //console.log(`Error: `, err) - // assert.include( - // err.message, - // `Input address must be a string or array of strings` - // ) - // } - // }) - // - // 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" - // ) - // // console.log(`balances: ${JSON.stringify(balances, null, 2)}`) - // - // assert.isArray(balances) - // assert.hasAllKeys(balances[0], [ - // "tokenId", - // "balanceString", - // "balance", - // "decimalCount", - // "slpAddress" - // ]) - // }) - // - // it(`should fetch balances for multiple addresses`, async () => { - // const addresses = [ - // "simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9", - // "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)}`) - // - // assert.isArray(balances) - // assert.isArray(balances[0]) - // assert.hasAllKeys(balances[0][0], [ - // "tokenId", - // "balanceString", - // "balance", - // "decimalCount", - // "slpAddress" - // ]) - // }) - // }) - // + describe("#tokenUtxoDetails", () => { + it("should return token details on a valid UTXO", async () => { + const utxos = [ + { + txid: + "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1", + vout: 1, + value: "546", + height: 597740, + confirmations: 1, + satoshis: 546 + } + ] + + const result = await bchjs.SLP.Utils.tokenUtxoDetails(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result[0], [ + "txid", + "vout", + "value", + "height", + "confirmations", + "satoshis", + "utxoType", + "transactionType", + "tokenId", + "tokenTicker", + "tokenName", + "tokenDocumentUrl", + "tokenDocumentHash", + "decimals", + "tokenQty", + "isValid" + ]) + }) + + it("should not choke on this problematic utxo", async () => { + const utxos = [ + { + txid: + "a8eb788b8ddda6faea00e6e2756624b8feb97655363d0400dd66839ea619d36e", + vout: 1, + value: "546", + height: 603282, + confirmations: 156, + satoshis: 546 + } + ] + + const result = await bchjs.SLP.Utils.tokenUtxoDetails(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result[0], [ + "txid", + "vout", + "value", + "height", + "confirmations", + "satoshis", + "utxoType", + "transactionType", + "tokenId", + "tokenTicker", + "tokenName", + "tokenDocumentUrl", + "tokenDocumentHash", + "decimals", + "tokenQty", + "isValid" + ]) + }) + + 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.tokenUtxoDetails(utxos) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 2) + assert.equal(result[0], false) + }) + + // 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.tokenUtxoDetails(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 () => { + const utxos = [ + { + txid: + "cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76", + vout: 1, + amount: 0.00000546, + satoshis: 546, + height: 600297, + confirmations: 76 + } + ] + + const data = await bchjs.SLP.Utils.tokenUtxoDetails(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], "batonStillExists") + 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.tokenUtxoDetails(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.tokenUtxoDetails(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.tokenUtxoDetails(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.tokenUtxoDetails(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 { + const address = 1234 + + await bchjs.SLP.Utils.balancesForAddress(address) + + assert.equal(true, false, "Uh oh. Code path should not end here.") + } catch (err) { + //console.log(`Error: `, err) + assert.include( + err.message, + `Input address must be a string or array of strings` + ) + } + }) + + 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" + ) + // console.log(`balances: ${JSON.stringify(balances, null, 2)}`) + + assert.isArray(balances) + assert.hasAllKeys(balances[0], [ + "tokenId", + "balanceString", + "balance", + "decimalCount", + "slpAddress" + ]) + }) + + it(`should fetch balances for multiple addresses`, async () => { + const addresses = [ + "simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9", + "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)}`) + + assert.isArray(balances) + assert.isArray(balances[0]) + assert.hasAllKeys(balances[0][0], [ + "tokenId", + "balanceString", + "balance", + "decimalCount", + "slpAddress" + ]) + }) + }) }) }) diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index 9c75d85..f916726 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -721,7 +721,7 @@ describe("#SLP Utils", () => { } }) - it("should throw error if utxo does not have satoshis property.", async () => { + it("should throw error if utxo does not have satoshis or value property.", async () => { try { const utxos = [ { @@ -749,13 +749,13 @@ describe("#SLP Utils", () => { } catch (err) { assert2.include( err.message, - `utxo 1 does not have a satoshis property`, + `utxo 1 does not have a satoshis or value property`, "Expected error message." ) } }) - it("should throw error if utxo does not have txid property.", async () => { + it("should throw error if utxo does not have txid or tx_hash property.", async () => { try { const utxos = [ { @@ -782,7 +782,7 @@ describe("#SLP Utils", () => { } catch (err) { assert2.include( err.message, - `utxo 1 does not have a txid property`, + `utxo 1 does not have a txid or tx_hash property`, "Expected error message." ) } @@ -790,39 +790,96 @@ describe("#SLP Utils", () => { // 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 false for change in an SLP token creation transaction", async () => { + // // Mock the call to the REST API + // if (process.env.TEST === "unit") { + // // Stub the call to validateTxid + // sandbox.stub(slp.Utils, "validateTxid").resolves([ + // { + // txid: + // "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + // valid: true + // }, + // { + // txid: + // "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", + // valid: true + // } + // ]) + // + // // Stub the calls to decodeOpReturn. + // sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({ + // tokenType: 1, + // transactionType: "genesis", + // ticker: "SLPSDK", + // name: "SLP SDK example using BITBOX", + // documentUrl: "developer.bitcoin.com", + // documentHash: "", + // decimals: 8, + // mintBatonVout: 2, + // initialQty: 507, + // tokensSentTo: + // "bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05", + // batonHolder: "bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05" + // }) + // } + // + // 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.isTokenUtxo(utxos) + // //console.log(`data: ${JSON.stringify(data, null, 2)}`) + // + // assert.equal( + // data[0], + // false, + // "Change should not be identified as SLP utxo." + // ) + // assert.equal(data[1], true, "SLP UTXO correctly identified.") + // }) it("should return false for change in an SLP token creation transaction", async () => { // Mock the call to the REST API - if (process.env.TEST === "unit") { - // Stub the call to validateTxid - sandbox.stub(slp.Utils, "validateTxid").resolves([ - { - txid: - "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", - valid: true - }, - { - txid: - "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90", - valid: true - } - ]) + // 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, - transactionType: "genesis", - ticker: "SLPSDK", - name: "SLP SDK example using BITBOX", - documentUrl: "developer.bitcoin.com", - documentHash: "", - decimals: 8, - mintBatonVout: 2, - initialQty: 507, - tokensSentTo: - "bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05", - batonHolder: "bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05" - }) - } + // Stub the calls to decodeOpReturn2. + 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 = [ { @@ -846,50 +903,38 @@ describe("#SLP Utils", () => { ] const data = await slp.Utils.isTokenUtxo(utxos) - //console.log(`data: ${JSON.stringify(data, null, 2)}`) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) assert.equal( - data[0], + data[0].isSlp, false, "Change should not be identified as SLP utxo." ) - assert.equal(data[1], true, "SLP UTXO correctly identified.") + assert.equal(data[1].isSlp, true, "SLP UTXO correctly identified.") }) it("should return true for a simple SEND SLP token utxo", async () => { // Mock the call to the REST API - if (process.env.TEST === "unit") { - // Stub the call to validateTxid - sandbox.stub(slp.Utils, "validateTxid").resolves([ - { - txid: - "fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb", - valid: true - } - ]) + // Stub the call to validateTxid + sandbox.stub(slp.Utils, "validateTxid").resolves([ + { + txid: + "fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb", + valid: true + } + ]) - // Stub the calls to decodeOpReturn. - sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({ - tokenType: 1, - transactionType: "send", - tokenId: - "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7", - spendData: [ - { - quantity: "200000000", - sentTo: "bitcoincash:qqll3st8xl0k8cgv8dgrrrkntv6hqdn8huv3xm2ztf", - vout: 1 - }, - { - quantity: "99887500000000", - sentTo: "bitcoincash:qzv7t2pzn2d0pklnetdjt65crh6fe8vnhuwvhsk2nn", - vout: 2 - } - ] - }) - } + // Stub the calls to decodeOpReturn. + sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({ + tokenType: 1, + txType: "SEND", + tokenId: + "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7", + amounts: ["200000000", "99887500000000"] + }) + // Input object. const utxos = [ { txid: @@ -903,9 +948,90 @@ describe("#SLP Utils", () => { ] const data = await slp.Utils.isTokenUtxo(utxos) - //console.log(`data: ${JSON.stringify(data, null, 2)}`) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) - assert.equal(data[0], true, "Simple send UTXO correctly identified") + assert.equal(data[0].isSlp, true, "Simple send UTXO correctly identified") + }) + + it("should accurately analyze non-slp UTXO from Electrumx", async () => { + try { + // Mock the call to the REST API + // Stub the call to validateTxid + sandbox.stub(slp.Utils, "validateTxid").resolves([ + { + txid: + "2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7", + valid: false + } + ]) + + // Simulate the utxos array returned by Electrumx. + const utxos = [ + { + height: 636885, + tx_hash: + "2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7", + tx_pos: 0, + value: 600 + } + ] + + const isTokenUtxos = await slp.Utils.isTokenUtxo(utxos) + // console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`) + + assert.equal( + isTokenUtxos[0].isSlp, + false, + "Non-slp UTXO correctly identified" + ) + } catch (err) { + console.log(`Error: `, err) + } + }) + + it("should accurately analyze slp UTXO from Electrumx", async () => { + try { + // Mock the call to the REST API + // Stub the call to validateTxid + sandbox.stub(slp.Utils, "validateTxid").resolves([ + { + txid: + "99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730", + valid: true + } + ]) + + // Stub the calls to decodeOpReturn. + sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({ + tokenType: 1, + txType: "SEND", + tokenId: + "a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2", + amounts: ["100", "198999900"] + }) + + // Simulate the utxos array returned by Electrumx. + const utxos = [ + { + height: 636885, + tx_hash: + "99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730", + tx_pos: 1, + value: 546 + } + ] + + const isTokenUtxos = await slp.Utils.isTokenUtxo(utxos) + // console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`) + + assert.equal( + isTokenUtxos[0].isSlp, + true, + "Slp UTXO correctly identified" + ) + } catch (err) { + console.log(`Error: `, err) + } }) }) From e36d99ca66d70f453dbba199db83c01fd6b2e044 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 28 May 2020 10:11:05 -0700 Subject: [PATCH 4/5] 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 { From 7cd23ca562a4af1c43447ef68812dbf9e2a6292c Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 31 May 2020 09:07:33 -0700 Subject: [PATCH 5/5] prototyping SLP send with slp-mdm --- src/slp/tokentype1.js | 65 +++++++++++++++++++++++++++++++++++++ test/unit/slp-tokentype1.js | 36 ++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/slp/tokentype1.js b/src/slp/tokentype1.js index b6704ab..58b1440 100644 --- a/src/slp/tokentype1.js +++ b/src/slp/tokentype1.js @@ -5,6 +5,8 @@ const Address = require("./address") const Script = require("../script") const BigNumber = require("bignumber.js") +const slpMdm = require("slp-mdm") + // const addy = new Address() let addy const TransactionBuilder = require("../transaction-builder") @@ -105,6 +107,69 @@ class TokenType1 { } } + // New version using slp-mdm + generateSendOpReturn2(tokenUtxos, sendQty) { + try { + const tokenId = tokenUtxos[0].tokenId + const decimals = tokenUtxos[0].decimals + + // 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 + // console.log(`change: ${change}`) + + let script + let outputs = 1 + + // The normal case, when there is token change to return to sender. + if (change > 0) { + outputs = 2 + + // Convert the send quantity to the format expected by slp-mdm. + let baseQty = new BigNumber(sendQty).times(10 ** decimals) + baseQty = baseQty.absoluteValue() + baseQty = Math.floor(baseQty) + baseQty = baseQty.toString() + console.log(`baseQty: `, baseQty) + + // Convert the change quantity to the format expected by slp-mdm. + let baseChange = new BigNumber(change).times(10 ** decimals) + baseChange = baseChange.absoluteValue() + baseChange = Math.floor(baseChange) + baseChange = baseChange.toString() + console.log(`baseChange: `, baseChange) + + // Generate the OP_RETURN as a Buffer. + script = slpMdm.TokenType1.send(tokenId, [ + new slpMdm.BN(baseQty), + new slpMdm.BN(baseChange) + ]) + // + + // Corner case, when there is no token change to send back. + } else { + let baseQty = new BigNumber(sendQty).times(10 ** decimals) + baseQty = baseQty.absoluteValue() + baseQty = Math.floor(baseQty) + baseQty = baseQty.toString() + console.log(`baseQty: `, baseQty) + + // console.log(`baseQty: ${baseQty.toString()}`) + + // Generate the OP_RETURN as a Buffer. + script = slpMdm.TokenType1.send(tokenId, [new slpMdm.BN(baseQty)]) + } + + return { script, outputs } + } catch (err) { + console.log(`Error in generateSendOpReturn2()`) + throw err + } + } + /** * @api SLP.TokenType1.generateBurnOpReturn() generateBurnOpReturn() - OP_RETURN code for SLP Send tx * @apiName generateBurnOpReturn diff --git a/test/unit/slp-tokentype1.js b/test/unit/slp-tokentype1.js index 53f19e2..9e20dc7 100644 --- a/test/unit/slp-tokentype1.js +++ b/test/unit/slp-tokentype1.js @@ -66,6 +66,7 @@ describe("#SLP TokenType1", () => { tokenUtxos, 1 ) + console.log(`result: ${JSON.stringify(result, null, 2)}`) assert.hasAllKeys(result, ["script", "outputs"]) assert.isNumber(result.outputs) @@ -153,6 +154,41 @@ describe("#SLP TokenType1", () => { }) }) + describe("#generateSendOpReturn2", () => { + it("should generate send OP_RETURN code", async () => { + // Mock UTXO. + const tokenUtxos = [ + { + txid: + "a8eb788b8ddda6faea00e6e2756624b8feb97655363d0400dd66839ea619d36e", + vout: 2, + value: "546", + confirmations: 0, + satoshis: 546, + utxoType: "token", + transactionType: "send", + tokenId: + "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7", + tokenTicker: "TOK-CH", + tokenName: "TokyoCash", + tokenDocumentUrl: "", + tokenDocumentHash: "", + decimals: 8, + tokenQty: 7 + } + ] + + const result = await bchjs.SLP.TokenType1.generateSendOpReturn2( + tokenUtxos, + 1 + ) + console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, ["script", "outputs"]) + assert.isNumber(result.outputs) + }) + }) + describe("#generateBurnOpReturn", () => { it("should generate burn OP_RETURN code", async () => { // Mock UTXO.