diff --git a/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js b/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js --- a/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js +++ b/web/cashtab/src/utils/__mocks__/mockTxBuilderObj.js @@ -474,3 +474,114 @@ bip68: {}, p2shInput: false, }; + +export const mockCreateTokenTxBuilderObj = { + transaction: { + prevTxMap: { + '582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d:0': 0, + 'f80e305c5c09585c67b4f395b153cd206083fdadb8687aa526611bcd381b5239:0': 1, + }, + network: { + hashGenesisBlock: + '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', + port: 8333, + portRpc: 8332, + protocol: { + magic: 3652501241, + }, + seedsDns: [ + 'seed.bitcoinabc.org', + 'seed-abc.bitcoinforks.org', + 'btccash-seeder.bitcoinunlimited.info', + 'seed.bitprim.org', + 'seed.deadalnix.me', + 'seeder.criptolayer.net', + ], + versions: { + bip32: { + private: 76066276, + public: 76067358, + }, + bip44: 145, + private: 128, + public: 0, + scripthash: 5, + messagePrefix: '\u0018BitcoinCash Signed Message:\n', + }, + name: 'BitcoinCash', + per1: 100000000, + unit: 'BCH', + testnet: false, + messagePrefix: '\u0018BitcoinCash Signed Message:\n', + bip32: { + public: 76067358, + private: 76066276, + }, + pubKeyHash: 0, + scriptHash: 5, + wif: 128, + dustThreshold: null, + }, + maximumFeeRate: 2500, + inputs: [{}, {}], + bitcoinCash: true, + tx: { + version: 2, + locktime: 0, + ins: [ + { + hash: { + type: 'Buffer', + data: [ + 88, 45, 250, 66, 226, 119, 138, 46, 107, 125, 50, + 251, 27, 244, 206, 252, 11, 233, 209, 10, 54, 83, + 142, 149, 3, 70, 93, 249, 156, 212, 166, 13, + ], + }, + index: 0, + script: { + type: 'Buffer', + data: [], + }, + sequence: 4294967295, + witness: [], + }, + { + hash: { + type: 'Buffer', + data: [ + 248, 14, 48, 92, 92, 9, 88, 92, 103, 180, 243, 149, + 177, 83, 205, 32, 96, 131, 253, 173, 184, 104, 122, + 165, 38, 97, 27, 205, 56, 27, 82, 57, + ], + }, + index: 0, + script: { + type: 'Buffer', + data: [], + }, + sequence: 4294967295, + witness: [], + }, + ], + outs: [], + }, + }, + DEFAULT_SEQUENCE: 4294967295, + hashTypes: { + SIGHASH_ALL: 1, + SIGHASH_NONE: 2, + SIGHASH_SINGLE: 3, + SIGHASH_ANYONECANPAY: 128, + SIGHASH_BITCOINCASH_BIP143: 64, + ADVANCED_TRANSACTION_MARKER: 0, + ADVANCED_TRANSACTION_FLAG: 1, + }, + signatureAlgorithms: { + ECDSA: 0, + SCHNORR: 1, + }, + bip66: {}, + bip68: {}, + p2shInput: false, +}; diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js --- a/web/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js @@ -22,6 +22,7 @@ generateOpReturnScript, generateTxInput, generateTxOutput, + generateTokenTxInput, signAndBuildTx, fromXecToSatoshis, getWalletBalanceFromUtxos, @@ -95,6 +96,7 @@ import { mockOneToOneSendXecTxBuilderObj, mockOneToManySendXecTxBuilderObj, + mockCreateTokenTxBuilderObj, } from '../__mocks__/mockTxBuilderObj'; import { mockSingleInputUtxo, @@ -512,6 +514,33 @@ expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input'); }); +it(`generateTokenTxInput() returns a valid object for a valid create token tx`, async () => { + const BCH = new BCHJS(); + let txBuilder = new BCH.TransactionBuilder(); + const tokenId = + '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e'; + const tokenInputObj = generateTokenTxInput( + BCH, + 'GENESIS', + mockNonSlpUtxos, + null, // no slpUtxos used for genesis tx + tokenId, + null, // no token send/burn amount for genesis tx + currency.defaultFee, + txBuilder, + ); + + expect(tokenInputObj.inputXecUtxos).toStrictEqual( + [mockNonSlpUtxos[0]].concat([mockNonSlpUtxos[1]]), + ); + expect(tokenInputObj.txBuilder.toString()).toStrictEqual( + mockCreateTokenTxBuilderObj.toString(), + ); + expect(tokenInputObj.remainderXecValue).toStrictEqual( + new BigNumber(699702), // remainder = tokenInputObj.inputXecUtxos - currency.etokenSats - txFee + ); +}); + it(`generateTxInput() returns an input object for a valid one to one XEC tx`, async () => { const BCH = new BCHJS(); const isOneToMany = false; diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js --- a/web/cashtab/src/utils/cashMethods.js +++ b/web/cashtab/src/utils/cashMethods.js @@ -97,6 +97,92 @@ return txInputObj; }; +export const generateTokenTxInput = ( + BCH, + tokenAction, // GENESIS, SEND or BURN + totalXecUtxos, + totalTokenUtxos, + tokenId, + tokenAmount, // optional - only for sending or burning + feeInSatsPerByte, + txBuilder, +) => { + let totalXecInputUtxoValue = new BigNumber(0); + let remainderXecValue = new BigNumber(0); + let totalXecInputUtxos = []; + let txFee = 0; + const { calcFee } = useBCH(); + + try { + if ( + !BCH || + !tokenAction || + !totalXecUtxos || + !tokenId || + !feeInSatsPerByte || + !txBuilder + ) { + throw new Error('Invalid token tx input parameter'); + } + + // collate XEC UTXOs for this token tx + const txOutputs = + tokenAction === 'GENESIS' + ? 2 // one for genesis OP_RETURN output and one for change + : 4; // for SEND/BURN token txs see T2645 on why this is not dynamically generated + for (let i = 0; i < totalXecUtxos.length; i++) { + const thisXecUtxo = totalXecUtxos[i]; + totalXecInputUtxoValue = totalXecInputUtxoValue.plus( + new BigNumber(thisXecUtxo.value), + ); + const vout = thisXecUtxo.outpoint.outIdx; + const txid = thisXecUtxo.outpoint.txid; + // add input with txid and index of vout + txBuilder.addInput(txid, vout); + + totalXecInputUtxos.push(thisXecUtxo); + txFee = calcFee( + BCH, + totalXecInputUtxos, + txOutputs, + feeInSatsPerByte, + ); + + remainderXecValue = totalXecInputUtxoValue + .minus(new BigNumber(currency.etokenSats)) + .minus(new BigNumber(txFee)); + + if (remainderXecValue.gte(0)) { + break; + } + } + + if (tokenAction === 'GENESIS') { + if (remainderXecValue.lt(0)) { + throw new Error(`Insufficient funds`); + } + } else { + // tokenAction is SEND or BURN + // to be added in part 2 of stacked diff + // loop through totalTokenUtxos to find all token utxos in wallet matching this token id + // collate enough token utxos to cover the amount of tokens to be sent or burnt + // add collated token utxos as inputs to txBuilder + // update txFee and XEC remainder based on token utxos + // calculate remainder tokens + } + } catch (err) { + console.log(`generateTokenTxInput() error: ` + err); + throw err; + } + + return { + txBuilder: txBuilder, + inputXecUtxos: totalXecInputUtxos, + inputTokenUtxos: totalTokenUtxos, + remainderXecValue: remainderXecValue, + }; +}; + export const getChangeAddressFromInputUtxos = (BCH, inputUtxos, wallet) => { if (!BCH || !inputUtxos || !wallet) { throw new Error('Invalid getChangeAddressFromWallet input parameter');