From b528a69c58fa8a5ce9ad83bd219bd3bd56c8468e Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 27 May 2020 11:56:36 -0700 Subject: [PATCH] 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) + } }) })