feat(decodeOpReturn2): Added slp-parser lib to create alternate decodeOpReturn

This commit is contained in:
Chris Troutner
2020-05-19 21:00:42 -07:00
parent 9e23ecb51e
commit f342f24e93
7 changed files with 524 additions and 2 deletions
+16
View File
@@ -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",
+2
View File
@@ -62,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",
+74
View File
@@ -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 = {
@@ -939,6 +941,78 @@ 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.
*
*/
// 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
+150
View File
@@ -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", () => {
+89 -1
View File
@@ -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
}
+2 -1
View File
@@ -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).
+191
View File
@@ -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)}`)
// })
// })
})