diff --git a/.gitignore b/.gitignore index 9c7b6ab..24df2d3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ wallet-info.txt wallet.json docs/ - +coverage/ # This paragraph comes last. Force includes specific files. !docs/README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cc6bd34 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Permissionless Software Foundation Community Contributing Guide 1.0 + +This document describes a very simple process suitable for most projects under +the PSF umbrella. It is based on [this Medium article](https://medium.com/the-node-js-collection/healthy-open-source-967fa8be7951) and the [Node.js Community Contribution Guide](https://github.com/nodejs/TSC/blob/master/BasePolicies/CONTRIBUTING.md). +Projects are encouraged to adopt this whether they +are hosted under the PSF or not. + +The goal of this document is to create a contribution process that: + +* Encourages new contributions. +* Encourages contributors to remain involved. +* Avoids unnecessary processes and bureaucracy whenever possible. +* Creates a transparent decision making process which makes it clear how +contributors can be involved in decision making. + +This document is based on much prior art in the Node.js community, io.js, +and the Node.js project. + +Additional guidance can be found at the [Permissionless Software Foundation Telegram Channel](https://t.me/permissionless_software). + +## Vocabulary + +* A **Contributor** is any individual creating or commenting on an issue or pull request. +* A **Committer** is a subset of contributors who have been given write access to the repository. +* A **TC (Technical Committee)** is a group of committers representing the required technical +expertise to resolve rare disputes. + +# Logging Issues + +Log an issue for any question or problem you might have. When in doubt, log an issue, +any additional policies about what to include will be provided in the responses. The only +exception is security disclosures which should be sent privately. + +Committers may direct you to another repository, ask for additional clarifications, and +add appropriate metadata before the issue is addressed. + +Please be courteous, respectful, and every participant is expected to follow the +project's Code of Conduct. + +# Contributions + +Any change to resources in this repository must be through pull requests. This applies to all changes +to documentation, code, binary files, etc. Even long term committers and TC members must use +pull requests. + +No pull request can be merged without being reviewed. + +For non-trivial contributions, pull requests should sit for at least 36 hours to ensure that +contributors in other timezones have time to review. Consideration should also be given to +weekends and other holiday periods to ensure active committers all have reasonable time to +become involved in the discussion and review process if they wish. + +The default for each contribution is that it is accepted once no committer has an objection. +During review committers may also request that a specific contributor who is most versed in a +particular area gives a "LGTM" before the PR can be merged. There is no additional "sign off" +process for contributions to land. Once all issues brought by committers are addressed it can +be landed by any committer. + +In the case of an objection being raised in a pull request by another committer, all involved +committers should seek to arrive at a consensus by way of addressing concerns being expressed +by discussion, compromise on the proposed change, or withdrawal of the proposed change. + +If a contribution is controversial and committers cannot agree about how to get it to land +or if it should land then it should be escalated to the TC. TC members should regularly +discuss pending contributions in order to find a resolution. It is expected that only a +small minority of issues be brought to the TC for resolution and that discussion and +compromise among committers be the default resolution mechanism. + +# Becoming a Committer + +All contributors who land a non-trivial contribution should be on-boarded in a timely manner, +and added as a committer, and be given write access to the repository. + +Committers are expected to follow this policy and continue to send pull requests, go through +proper review, and have other committers merge their pull requests. + +# TC Process + +The TC uses a "consensus seeking" process for issues that are escalated to the TC. +The group tries to find a resolution that has no open objections among TC members. +If a consensus cannot be reached that has no objections then a majority wins vote +is called. It is also expected that the majority of decisions made by the TC are via +a consensus seeking process and that voting is only used as a last-resort. + +Resolution may involve returning the issue to committers with suggestions on how to +move forward towards a consensus. It is not expected that a meeting of the TC +will resolve all issues on its agenda during that meeting and may prefer to continue +the discussion happening among the committers. + +Members can be added to the TC at any time. Any committer can nominate another committer +to the TC and the TC uses its standard consensus seeking process to evaluate whether or +not to add this new member. Members who do not participate consistently at the level of +a majority of the other members are expected to resign. diff --git a/package-lock.json b/package-lock.json index 37b5fda..b278ecd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10130,6 +10130,22 @@ "is-fullwidth-code-point": "^2.0.0" } }, + "slp-mdm": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/slp-mdm/-/slp-mdm-0.0.6.tgz", + "integrity": "sha512-fbjlIg/o8OtzgK2JydC6POJp3Qup/rLgy4yB5hoLgxWRlERyJyE29ScwS3r9TTwPxe12qK55pyivAdNOZZXL0A==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "slp-parser": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slp-parser/-/slp-parser-0.0.4.tgz", + "integrity": "sha512-AvbslJumkzGfMGWNvuE2pWx2nyHEk/VgQ7l119kDKIFRTuRUWOkyOULLauw5laGRQsBRThg6NCx/TsR3grX6GA==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "socket.io": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", diff --git a/package.json b/package.json index ef84332..d955b6b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:integration:testnet:local": "RESTURL=http://localhost:4000/v3/ mocha --timeout 30000 test/integration/testnet", "test:integration:api": "RESTURL=https://api.fullstack.cash/v3/ mocha --timeout 30000 test/integration", "coverage": "nyc report --reporter=text-lcov | coveralls", + "coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/", "docs": "./node_modules/.bin/apidoc -i src/ -o docs" }, "license": "MIT", @@ -61,6 +62,8 @@ "repl.history": "^0.1.4", "safe-buffer": "^5.1.2", "satoshi-bitcoin": "^1.0.4", + "slp-mdm": "0.0.6", + "slp-parser": "0.0.4", "socket.io": "^2.1.1", "socket.io-client": "^2.1.1", "touch": "^3.1.0", diff --git a/src/slp/utils.js b/src/slp/utils.js index ea5bc86..5a35449 100644 --- a/src/slp/utils.js +++ b/src/slp/utils.js @@ -1,4 +1,5 @@ const axios = require("axios") +const slpParser = require("slp-parser") const Script = require("../script") const scriptLib = new Script() @@ -11,6 +12,7 @@ class Utils { constructor(config) { this.restURL = config.restURL this.apiToken = config.apiToken + this.slpParser = slpParser // Add JWT token to the authorization header. this.axiosOptions = { @@ -740,6 +742,9 @@ class Utils { * * Throws an error if given a non-SLP txid. * + * Note: At some point, this method will be deprecated in favor of decodeOpReturn2(). + * At that time, decodeOpReturn2() will be rename to decodeOpReturn(). + * * @apiExample Example usage: * * (async () => { @@ -939,6 +944,106 @@ class Utils { } } + /** + * @api SLP.Utils.decodeOpReturn2() decodeOpReturn2() - Read the OP_RETURN data from an SLP transaction. + * @apiName decodeOpReturn2 + * @apiGroup SLP + * @apiDescription Retrieves transactions data from a txid and decodes the SLP OP_RETURN data. + * + * Similar to decodeOpReturn(), except decodeOpReturn2() uses the slp-parser + * library maintained by JT Freeman. Outputs have slightly different format. + * + * Throws an error if given a non-SLP txid. + * + * In a future version of bch-js, this method will replace the origonal + * decodeOpReturn() method. + * + * @apiExample Example usage: + * + * (async () => { + * try { + * const txid = + * "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1" + * + * const data = await slp.Utils.decodeOpReturn2(txid) + * + * console.log(`Decoded OP_RETURN data: ${JSON.stringify(data,null,2)}`) + * } catch (error) { + * console.error(error) + * } + * })() + * + * // returns + * { + * "tokenType": 1, + * "txType": "SEND", + * "tokenId": "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7" + * "amounts": [ + * "100000000", + * "99883300000000" + * ] + * } + */ + // Reimplementation of decodeOpReturn() using slp-parser. + async decodeOpReturn2(txid) { + try { + // Validate the txid input. + if (!txid || txid === "" || typeof txid !== "string") + throw new Error(`txid string must be included.`) + + // Retrieve the transaction object from the full node. + const path = `${this.restURL}rawtransactions/getRawTransaction/${txid}?verbose=true` + const response = await axios.get(path, _this.axiosOptions) + const txDetails = response.data + // console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`) + + // SLP spec expects OP_RETURN to be the first output of the transaction. + const opReturn = txDetails.vout[0].scriptPubKey.hex + // console.log(`opReturn hex: ${opReturn}`) + + const parsedData = _this.slpParser.parseSLP(Buffer.from(opReturn, "hex")) + // console.log(`parsedData: ${JSON.stringify(parsedData, null, 2)}`) + + // Convert Buffer data to hex strings or utf8 strings. + let tokenData = {} + if (parsedData.transactionType === "SEND") { + tokenData = { + tokenType: parsedData.tokenType, + txType: parsedData.transactionType, + tokenId: parsedData.data.tokenId.toString("hex"), + amounts: parsedData.data.amounts + } + } else if (parsedData.transactionType === "GENESIS") { + tokenData = { + tokenType: parsedData.tokenType, + txType: parsedData.transactionType, + ticker: parsedData.data.ticker.toString(), + name: parsedData.data.name.toString(), + tokenId: txid, + documentUri: parsedData.data.documentUri.toString(), + documentHash: parsedData.data.documentHash.toString(), + decimals: parsedData.data.decimals, + mintBatonVout: parsedData.data.mintBatonVout, + qty: parsedData.data.qty + } + } else if (parsedData.transactionType === "MINT") { + tokenData = { + tokenType: parsedData.tokenType, + txType: parsedData.transactionType, + tokenId: parsedData.data.tokenId.toString("hex"), + mintBatonVout: parsedData.data.mintBatonVout, + qty: parsedData.data.qty + } + } + // console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`) + + return tokenData + } catch (error) { + if (error.response && error.response.data) throw error.response.data + throw error + } + } + /** * @api SLP.Utils.isTokenUtxo() isTokenUtxo() - Determine if UTXO belongs to an SLP transaction. * @apiName isTokenUtxo diff --git a/test/integration/slp.js b/test/integration/slp.js index 69aacad..1bfb332 100644 --- a/test/integration/slp.js +++ b/test/integration/slp.js @@ -75,6 +75,156 @@ describe(`#SLP`, () => { "spendData" ]) }) + + it("should decode the OP_RETURN for a GENESIS txid", async () => { + const txid = + "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7" + + const result = await bchjs.SLP.Utils.decodeOpReturn(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, [ + "tokenType", + "transactionType", + "ticker", + "name", + "documentUrl", + "documentHash", + "decimals", + "mintBatonVout", + "initialQty", + "tokensSentTo", + "batonHolder" + ]) + }) + + it("should decode the OP_RETURN for a MINT txid", async () => { + const txid = + "65f21bbfcd545e5eb515e38e861a9dfe2378aaa2c4e458eb9e59e4d40e38f3a4" + + const result = await bchjs.SLP.Utils.decodeOpReturn(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, [ + "tokenType", + "transactionType", + "tokenId", + "mintBatonVout", + "batonStillExists", + "quantity", + "tokensSentTo", + "batonHolder" + ]) + }) + + it("should throw an error for a non-SLP transaction", async () => { + try { + const txid = + "3793d4906654f648e659f384c0f40b19c8f10c1e9fb72232a9b8edd61abaa1ec" + + await bchjs.SLP.Utils.decodeOpReturn(txid) + + assert.equal(true, false, "Unexpected result") + } catch (err) { + // console.log(`err: `, err) + assert.include(err.message, "Not an OP_RETURN") + } + }) + }) + + describe("#decodeOpReturn2", () => { + it("should decode the OP_RETURN for a SEND txid", async () => { + const txid = + "266844d53e46bbd7dd37134688dffea6e54d944edff27a0add63dd0908839bc1" + + const result = await bchjs.SLP.Utils.decodeOpReturn2(txid) + console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, ["tokenType", "txType", "tokenId", "amounts"]) + assert.equal( + result.tokenId, + "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7" + ) + + // Verify outputs + assert.equal(result.amounts.length, 2) + assert.equal(result.amounts[0], "100000000") + assert.equal(result.amounts[1], "99883300000000") + }) + + it("should decode the OP_RETURN for a GENESIS txid", async () => { + const txid = + "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7" + + const result = await bchjs.SLP.Utils.decodeOpReturn2(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, [ + "tokenType", + "txType", + "tokenId", + "ticker", + "name", + "documentUri", + "documentHash", + "decimals", + "mintBatonVout", + "qty" + ]) + assert.equal( + result.tokenId, + "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7" + ) + assert.equal(result.txType, "GENESIS") + assert.equal(result.ticker, "TOK-CH") + assert.equal(result.name, "TokyoCash") + }) + + it("should decode the OP_RETURN for a MINT txid", async () => { + const txid = + "65f21bbfcd545e5eb515e38e861a9dfe2378aaa2c4e458eb9e59e4d40e38f3a4" + + const result = await bchjs.SLP.Utils.decodeOpReturn2(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, [ + "tokenType", + "txType", + "tokenId", + "mintBatonVout", + "qty" + ]) + }) + + it("should throw an error for a non-SLP transaction", async () => { + try { + const txid = + "3793d4906654f648e659f384c0f40b19c8f10c1e9fb72232a9b8edd61abaa1ec" + + await bchjs.SLP.Utils.decodeOpReturn2(txid) + + assert.equal(true, false, "Unexpected result") + } catch (err) { + // console.log(`err: `, err) + assert.include(err.message, "scriptpubkey not op_return") + } + }) + + // Note: This TX is interpreted as valid by the original decodeOpReturn(). + // Fixing this issue and related issues was the reason for creating the + // decodeOpReturn2() method using the slp-parser library. + it("should throw error for invalid SLP transaction", async () => { + try { + const txid = + "a60a522cc11ad7011b74e57fbabbd99296e4b9346bcb175dcf84efb737030415" + + await bchjs.SLP.Utils.decodeOpReturn2(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + } catch (err) { + // console.log(`err: `, err) + assert.include(err.message, "amount string size not 8 bytes") + } + }) }) describe("#tokenUtxoDetails", () => { diff --git a/test/unit/fixtures/slp/mock-utils.js b/test/unit/fixtures/slp/mock-utils.js index 8259fbc..ea1b233 100644 --- a/test/unit/fixtures/slp/mock-utils.js +++ b/test/unit/fixtures/slp/mock-utils.js @@ -944,6 +944,93 @@ const mockDualOpData = { ] } +const mockInvalidSlpSend = { + txid: "a60a522cc11ad7011b74e57fbabbd99296e4b9346bcb175dcf84efb737030415", + hash: "a60a522cc11ad7011b74e57fbabbd99296e4b9346bcb175dcf84efb737030415", + version: 2, + size: 473, + locktime: 0, + vin: [ + { + txid: "3ad621d46ddb7bdccb6e5e7b6505ee639d9327a2b2bdaa2937b8e3aa55c4f2a7", + vout: 0, + scriptSig: { + asm: + "3045022100f962fdc585261007f114f0470828bbe9ac6197cefc4d0897fe4a7dd1d4cb2f5402202e104b617a1c14fc293c0ace9d4fd5e1cfa819a68b4ff976b4c3c199feda5227[ALL|FORKID] 0351c450ded2d7747dcad69982c9d954f3f785bab018d89fa09723bba7397e91fb", + hex: + "483045022100f962fdc585261007f114f0470828bbe9ac6197cefc4d0897fe4a7dd1d4cb2f5402202e104b617a1c14fc293c0ace9d4fd5e1cfa819a68b4ff976b4c3c199feda522741210351c450ded2d7747dcad69982c9d954f3f785bab018d89fa09723bba7397e91fb" + }, + sequence: 4294967295 + }, + { + txid: "a31c3167686f892d835727bc46b74bb11a46d74a8a6b08dc6cd82abb6e987b43", + vout: 1, + scriptSig: { + asm: + "3044022027dcd4ef752819ba4e61b892039ea0cb8d3ab0ffaeb78384dce9cf43dd76967e0220594fdf41e3d26cd207b6df939e3d655f6e09b01f05948edeb096aa644e1b6ec4[ALL|FORKID] 0351c450ded2d7747dcad69982c9d954f3f785bab018d89fa09723bba7397e91fb", + hex: + "473044022027dcd4ef752819ba4e61b892039ea0cb8d3ab0ffaeb78384dce9cf43dd76967e0220594fdf41e3d26cd207b6df939e3d655f6e09b01f05948edeb096aa644e1b6ec441210351c450ded2d7747dcad69982c9d954f3f785bab018d89fa09723bba7397e91fb" + }, + sequence: 4294967295 + } + ], + vout: [ + { + value: 0, + n: 0, + scriptPubKey: { + asm: + "OP_RETURN 5262419 1 1145980243 091c80cee60cc3a6dd7b8c6c04cf0cf2d8103ba2d75daa105dcf1aaa53551fe8 1 fffffffffffffffe", + hex: + "6a04534c500001010453454e4420091c80cee60cc3a6dd7b8c6c04cf0cf2d8103ba2d75daa105dcf1aaa53551fe8010108fffffffffffffffe", + type: "nulldata" + } + }, + { + value: 0.00000546, + n: 1, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 c0c53d84b5420c9bfe4015d1ae69017c3f1ddef8 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a914c0c53d84b5420c9bfe4015d1ae69017c3f1ddef888ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: ["bitcoincash:qrqv20vyk4pqexl7gq2artnfq97r78w7lqj5548mny"] + } + }, + { + value: 0.00000546, + n: 2, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 b505afc357a7e911b207332f13e8e674a72099c3 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a914b505afc357a7e911b207332f13e8e674a72099c388ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: ["bitcoincash:qz6stt7r27n7jydjquej7ylgue62wgyecvs9zm4gff"] + } + }, + { + value: 0.00044091, + n: 3, + scriptPubKey: { + asm: + "OP_DUP OP_HASH160 b505afc357a7e911b207332f13e8e674a72099c3 OP_EQUALVERIFY OP_CHECKSIG", + hex: "76a914b505afc357a7e911b207332f13e8e674a72099c388ac", + reqSigs: 1, + type: "pubkeyhash", + addresses: ["bitcoincash:qz6stt7r27n7jydjquej7ylgue62wgyecvs9zm4gff"] + } + } + ], + hex: + "0200000002a7f2c455aae3b83729aabdb2a227939d63ee05657b5e6ecbdc7bdb6dd421d63a000000006b483045022100f962fdc585261007f114f0470828bbe9ac6197cefc4d0897fe4a7dd1d4cb2f5402202e104b617a1c14fc293c0ace9d4fd5e1cfa819a68b4ff976b4c3c199feda522741210351c450ded2d7747dcad69982c9d954f3f785bab018d89fa09723bba7397e91fbffffffff437b986ebb2ad86cdc086b8a4ad7461ab14bb746bc2757832d896f6867311ca3010000006a473044022027dcd4ef752819ba4e61b892039ea0cb8d3ab0ffaeb78384dce9cf43dd76967e0220594fdf41e3d26cd207b6df939e3d655f6e09b01f05948edeb096aa644e1b6ec441210351c450ded2d7747dcad69982c9d954f3f785bab018d89fa09723bba7397e91fbffffffff040000000000000000396a04534c500001010453454e4420091c80cee60cc3a6dd7b8c6c04cf0cf2d8103ba2d75daa105dcf1aaa53551fe8010108fffffffffffffffe22020000000000001976a914c0c53d84b5420c9bfe4015d1ae69017c3f1ddef888ac22020000000000001976a914b505afc357a7e911b207332f13e8e674a72099c388ac3bac0000000000001976a914b505afc357a7e911b207332f13e8e674a72099c388ac00000000", + blockhash: "000000000000000000915bd33a7241f34800b190f7cf51f90b42b2b05d2b7ed8", + confirmations: 90327, + time: 1535577007, + blocktime: 1535577007 +} + module.exports = { mockList, mockToken, @@ -966,5 +1053,6 @@ module.exports = { txDetailsSLPSendAlt, mockTxDetails, mockDualValidation, - mockDualOpData + mockDualOpData, + mockInvalidSlpSend } diff --git a/test/unit/slp-tokentype1.js b/test/unit/slp-tokentype1.js index 21aae9e..53f19e2 100644 --- a/test/unit/slp-tokentype1.js +++ b/test/unit/slp-tokentype1.js @@ -99,7 +99,8 @@ describe("#SLP TokenType1", () => { 5000 ) // console.log(`result: ${JSON.stringify(result, null, 2)}`) - // console.log(`result.script: `, result.script) + console.log(`result.script: `, result.script) + console.log(`result.script: `, result.script.toString("hex")) // This transaction failed due to a floating point error. This is expressed // by the script[6] being length 2 (incorrect) instead of 8 (correct). diff --git a/test/unit/slp-utils.js b/test/unit/slp-utils.js index 6083e48..115a407 100644 --- a/test/unit/slp-utils.js +++ b/test/unit/slp-utils.js @@ -525,6 +525,187 @@ describe("#SLP Utils", () => { }) }) + describe("#decodeOpReturn2", () => { + it("should throw an error for a non-string input", async () => { + try { + const txid = 53423 // Not a string. + + await slp.Utils.decodeOpReturn2(txid) + + assert2.equal(true, false, "Unexpected result.") + } catch (err) { + //console.log(`err: ${util.inspect(err)}`) + assert2.include(err.message, `txid string must be included`) + } + }) + + it("should throw an error for non-SLP transaction", async () => { + try { + // Mock the call to the REST API + if (process.env.TEST === "unit") { + sandbox + .stub(axios, "get") + .resolves({ data: mockData.nonSLPTxDetailsWithoutOpReturn }) + } + + const txid = + "3793d4906654f648e659f384c0f40b19c8f10c1e9fb72232a9b8edd61abaa1ec" + + await slp.Utils.decodeOpReturn2(txid) + + assert2.equal(true, false, "Unexpected result.") + } catch (err) { + // console.log(`err: ${util.inspect(err)}`) + assert2.include(err.message, `scriptpubkey not op_return`) + } + }) + + it("should throw an error for non-SLP transaction with OP_RETURN", async () => { + try { + // Mock the call to the REST API + if (process.env.TEST === "unit") { + sandbox + .stub(axios, "get") + .resolves({ data: mockData.nonSLPTxDetailsWithOpReturn }) + } + + const txid = + "2ff74c48a5d657cf45f699601990bffbbe7a2a516d5480674cbf6c6a4497908f" + + await slp.Utils.decodeOpReturn2(txid) + + assert2.equal(true, false, "Unexpected result.") + } catch (err) { + // console.log(`err: ${util.inspect(err)}`) + assert2.include(err.message, `SLP not in first chunk`) + } + }) + + it("should decode a genesis transaction", async () => { + // Mock the call to the REST API + if (process.env.TEST === "unit") { + sandbox + .stub(axios, "get") + .resolves({ data: mockData.txDetailsSLPGenesis }) + } + + const txid = + "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90" + + const result = await slp.Utils.decodeOpReturn2(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert2.hasAllKeys(result, [ + "tokenType", + "txType", + "tokenId", + "ticker", + "name", + "documentUri", + "documentHash", + "decimals", + "mintBatonVout", + "qty" + ]) + }) + + it("should decode a mint transaction", async () => { + // Mock the call to the REST API + if (process.env.TEST === "unit") + sandbox.stub(axios, "get").resolves({ data: mockData.txDetailsSLPMint }) + + const txid = + "65f21bbfcd545e5eb515e38e861a9dfe2378aaa2c4e458eb9e59e4d40e38f3a4" + + const result = await slp.Utils.decodeOpReturn2(txid) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert2.hasAllKeys(result, [ + "tokenType", + "txType", + "tokenId", + "mintBatonVout", + "qty" + ]) + }) + + it("should decode a send transaction", async () => { + // Mock the call to the REST API + if (process.env.TEST === "unit") + sandbox.stub(axios, "get").resolves({ data: mockData.txDetailsSLPSend }) + + const txid = + "4f922565af664b6fdf0a1ba3924487344be721b3d8815c62cafc8a51e04a8afa" + + const result = await slp.Utils.decodeOpReturn2(txid) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert2.hasAllKeys(result, ["tokenType", "txType", "tokenId", "amounts"]) + }) + + it("should properly decode a Genesis transaction with no minting baton", async () => { + // Mock the call to the REST API. + if (process.env.TEST === "unit") { + sandbox + .stub(axios, "get") + .resolves({ data: mockData.txDetailsSLPGenesisNoBaton }) + } + + const txid = + "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7" + + const data = await slp.Utils.decodeOpReturn2(txid) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert2.equal(data.mintBatonVout, 0) + }) + + it("should decode a send transaction with alternate encoding", async () => { + // Mock the call to rest.bitcoin.com + if (process.env.TEST === "unit") { + sandbox + .stub(axios, "get") + .resolves({ data: mockData.txDetailsSLPSendAlt }) + } + + const txid = + "d94357179775425ebc59c93173bd6dc9854095f090a2eb9dcfe9797398bc8eae" + + const data = await slp.Utils.decodeOpReturn2(txid) + // console.log(`data: ${JSON.stringify(data, null, 2)}`) + + assert2.hasAnyKeys(data, [ + "transactionType", + "txType", + "tokenId", + "amounts" + ]) + }) + + // Note: This TX is interpreted as valid by the original decodeOpReturn(). + // Fixing this issue and related issues was the reason for creating the + // decodeOpReturn2() method using the slp-parser library. + it("should throw error for invalid SLP transaction", async () => { + try { + // Mock the call to rest.bitcoin.com + if (process.env.TEST === "unit") { + sandbox + .stub(axios, "get") + .resolves({ data: mockData.mockInvalidSlpSend }) + } + + const txid = + "a60a522cc11ad7011b74e57fbabbd99296e4b9346bcb175dcf84efb737030415" + + await slp.Utils.decodeOpReturn2(txid) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + } catch (err) { + // console.log(`err: `, err) + assert2.include(err.message, "amount string size not 8 bytes") + } + }) + }) + describe("#isTokenUtxo", () => { it("should throw error if input is not an array.", async () => { try { @@ -1277,4 +1458,14 @@ describe("#SLP Utils", () => { ]) }) }) + + // describe("#decodeOpReturn2", () => { + // it("should do something", async () => { + // const txid = + // "4f922565af664b6fdf0a1ba3924487344be721b3d8815c62cafc8a51e04a8afa" + // + // await slp.Utils.decodeOpReturn2(txid) + // // console.log(`result: ${JSON.stringify(result, null, 2)}`) + // }) + // }) })