From e881d24d08c554ad36c3afa411e07a2314141770 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 8 Jul 2020 10:32:07 -0700 Subject: [PATCH] fix(generateSendOpReturnWeb): Restoring old SLP Send for web --- src/slp/tokentype1.js | 86 ++++++++++++++++++++++++++ test/unit/slp-tokentype1.js | 119 ++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) diff --git a/src/slp/tokentype1.js b/src/slp/tokentype1.js index ec85024..061e672 100644 --- a/src/slp/tokentype1.js +++ b/src/slp/tokentype1.js @@ -2,6 +2,7 @@ //const bchjs = new BCHJS() const Address = require("./address") +const Script = require("../script") const BigNumber = require("bignumber.js") const slpMdm = require("slp-mdm") @@ -15,6 +16,7 @@ class TokenType1 { this.restURL = config.restURL addy = new Address(config) + this.Script = new Script() // Instantiate the transaction builder. TransactionBuilder.setAddress(addy) @@ -123,6 +125,90 @@ class TokenType1 { } } + /** + * @api SLP.TokenType1.generateSendOpReturnWeb() generateSendOpReturnWeb() - Web-friendly OP_RETURN code for SLP Send tx + * @apiName generateSendOpReturnWeb + * @apiGroup SLP + * @apiDescription Generate the OP_RETURN value needed to create an SLP Send transaction. + * It's assumed all elements in the tokenUtxos array belong to the same token. + * Returns an object with two properties: + * - script: an array of Bufers that is ready to fed into bchjs.Script.encode2() to be turned into a transaction output. + * - outputs: an integer with a value of 1 or 2. If 2, indicates there needs to be an extra output to send token change. + */ + generateSendOpReturnWeb(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 + + let baseQty = new BigNumber(sendQty).times(10 ** decimals) + baseQty = baseQty.absoluteValue() + baseQty = Math.floor(baseQty) + let baseQtyHex = baseQty.toString(16) + baseQtyHex = baseQtyHex.padStart(16, "0") + + let baseChange = new BigNumber(change).times(10 ** decimals) + baseChange = baseChange.absoluteValue() + baseChange = Math.floor(baseChange) + // console.log(`baseChange: ${baseChange.toString()}`) + + let baseChangeHex = baseChange.toString(16) + baseChangeHex = baseChangeHex.padStart(16, "0") + // console.log(`baseChangeHex padded: ${baseChangeHex}`) + + script = [ + this.Script.opcodes.OP_RETURN, + Buffer.from("534c5000", "hex"), + //BITBOX.Script.opcodes.OP_1, + Buffer.from("01", "hex"), + Buffer.from(`SEND`), + Buffer.from(tokenId, "hex"), + Buffer.from(baseQtyHex, "hex"), + Buffer.from(baseChangeHex, "hex") + ] + } else { + // Corner case, when there is no token change to send back. + + let baseQty = new BigNumber(sendQty).times(10 ** decimals) + baseQty = baseQty.absoluteValue() + baseQty = Math.floor(baseQty) + let baseQtyHex = baseQty.toString(16) + baseQtyHex = baseQtyHex.padStart(16, "0") + + // console.log(`baseQty: ${baseQty.toString()}`) + + script = [ + this.Script.opcodes.OP_RETURN, + Buffer.from("534c5000", "hex"), + //BITBOX.Script.opcodes.OP_1, + Buffer.from("01", "hex"), + Buffer.from(`SEND`), + Buffer.from(tokenId, "hex"), + Buffer.from(baseQtyHex, "hex") + ] + } + + return { script, outputs } + } catch (err) { + console.log(`Error in generateSendOpReturnWeb()`) + throw err + } + } + /** * @api SLP.TokenType1.generateBurnOpReturn() generateBurnOpReturn() * @apiName generateBurnOpReturn diff --git a/test/unit/slp-tokentype1.js b/test/unit/slp-tokentype1.js index b0edd6d..46fd5dc 100644 --- a/test/unit/slp-tokentype1.js +++ b/test/unit/slp-tokentype1.js @@ -70,6 +70,125 @@ describe("#SLP TokenType1", () => { }) }) + describe("#generateSendOpReturnWeb", () => { + 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.generateSendOpReturnWeb( + tokenUtxos, + 1 + ) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, ["script", "outputs"]) + assert.isNumber(result.outputs) + }) + + it("should handle problematic configuration", async () => { + // Mock UTXO. + const tokenUtxos = [ + { + txid: + "0bd6b874246202b4cbc2f501419a5ce6f9b01e8ba9521298afd15c7e8eac5951", + vout: 2, + value: "546", + confirmations: 0, + satoshis: 546, + utxoType: "token", + transactionType: "send", + tokenId: + "155784a206873c98acc09e8dabcccf6abf13c4c14d8662190534138a16bb93ce", + tokenTicker: "PSF", + tokenName: "PSF Testnet Token", + tokenDocumentUrl: "", + tokenDocumentHash: "", + decimals: 8, + tokenQty: 18004.71169917 + } + ] + + const result = await bchjs.SLP.TokenType1.generateSendOpReturnWeb( + tokenUtxos, + 5000 + ) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + // 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). + assert.equal(result.script[5].length, 8) + assert.equal(result.script[6].length, 8) + }) + + it("should generate good OP_RETURN for problematic TX", () => { + const utxo1 = { + txid: + "35287aa2aa7d3f1954fbbcb8a748d8773359b4f2771a851959a4a6116bcb552c", + vout: 1, + amount: 0.00000546, + satoshis: 546, + legacyAddress: "1MuLZ9LzLSTKioBHoPzeSfFANTEN2CZhuS", + cashAddress: "bitcoincash:qrj5sala6z53v559q54mekexru2xe34yrquhmpy5kx", + tokenId: + "c4b0d62156b3fa5c8f3436079b5394f7edc1bef5dc1cd2f9d0c4d46f82cca479", + decimals: 2, + tokenQty: 1371.6 + } + + const utxo2 = { + txid: + "cb652ce62babc81936f0f1e1d5b80102cb44de99dece83f4bbd34cfaeef744b0", + vout: 2, + amount: 0.00000546, + satoshis: 546, + legacyAddress: "1LzjkvDVB16k6kcSz8hZodW9KPP2zxwmGh", + cashAddress: "bitcoincash:qrd4tj48lpf0wv36qt5mkd6c3n4h5xcfgqqw77s63d", + tokenId: + "c4b0d62156b3fa5c8f3436079b5394f7edc1bef5dc1cd2f9d0c4d46f82cca479", + decimals: 2, + tokenQty: 3250.75 + } + + const utxos = [utxo1, utxo2] + + const { script } = bchjs.SLP.TokenType1.generateSendOpReturnWeb( + utxos, + 1400 + ) + + // console.log(`script: `, script) + // console.log(`outputs: `, outputs) + + // console.log(`script[6]: ${script[6].toString("hex")}`) + // console.log(`script[6].length: ${script[6].length}`) + + // This transaction failed due to a floating point error. This is expressed + // by the script[6] being length 2 (incorrect) instead of 8 (correct). + assert.equal(script[6].length, 8) + }) + }) + describe("#generateBurnOpReturn", () => { it("should generate burn OP_RETURN code", () => { // Mock UTXO.