diff --git a/modules/sdk-api/src/v1/transactionBuilder.ts b/modules/sdk-api/src/v1/transactionBuilder.ts index be03a14d68..9bfca327af 100644 --- a/modules/sdk-api/src/v1/transactionBuilder.ts +++ b/modules/sdk-api/src/v1/transactionBuilder.ts @@ -21,6 +21,46 @@ import { common, getAddressP2PKH, getNetwork, sanitizeLegacyPath } from '@bitgo/ import { verifyAddress } from './verifyAddress'; import { tryPromise } from '../util'; +type Triple = [T, T, T]; + +interface V1Keychain { + xpub: string; + path?: string; + walletSubPath?: string; +} + +/** + * Parse chainPath like "/0/13" into { chain: 0, index: 13 } + */ +function parseChainPath(chainPath: string): { chain: number; index: number } { + const parts = chainPath.split('/').filter((p) => p.length > 0); + if (parts.length !== 2) { + throw new Error(`Invalid chainPath: ${chainPath}`); + } + return { chain: parseInt(parts[0], 10), index: parseInt(parts[1], 10) }; +} + +/** + * Create RootWalletKeys from v1 wallet keychains. + * v1 keychains have a structure like { xpub, path: 'm', walletSubPath: '/0/0' } + */ +function createRootWalletKeysFromV1Keychains(keychains: V1Keychain[]): utxolib.bitgo.RootWalletKeys { + if (keychains.length !== 3) { + throw new Error('Expected 3 keychains for v1 wallet'); + } + + const bip32Keys = keychains.map((k) => bip32.fromBase58(k.xpub)) as Triple; + + // v1 wallets typically have walletSubPath like '/0/0' which we convert to derivation prefixes like '0/0' + const derivationPrefixes = keychains.map((k) => { + const walletSubPath = k.walletSubPath || '/0/0'; + // Remove leading slash if present + return walletSubPath.startsWith('/') ? walletSubPath.slice(1) : walletSubPath; + }) as Triple; + + return new utxolib.bitgo.RootWalletKeys(bip32Keys, derivationPrefixes); +} + interface BaseOutput { amount: number; travelInfo?: any; @@ -235,6 +275,9 @@ exports.createTransaction = function (params) { let changeOutputs: Output[] = []; + // All outputs for the transaction (recipients, OP_RETURNs, change, fees) + let outputs: Output[] = []; + let containsUncompressedPublicKeys = false; // The transaction. @@ -603,7 +646,8 @@ exports.createTransaction = function (params) { throw new Error('transaction too large: estimated size ' + minerFeeInfo.size + ' bytes'); } - const outputs: Output[] = []; + // Reset outputs array (use outer scope variable) + outputs = []; recipients.forEach(function (recipient) { let script; @@ -739,8 +783,75 @@ exports.createTransaction = function (params) { }); }; + // Build PSBT with all signing metadata embedded + const buildPsbt = function (): utxolib.bitgo.UtxoPsbt { + const psbt = utxolib.bitgo.createPsbtForNetwork({ network }); + + // Need wallet keychains for PSBT metadata + const walletKeychains = params.wallet.keychains; + if (!walletKeychains || walletKeychains.length !== 3) { + throw new Error('Wallet keychains required for PSBT format'); + } + + const rootWalletKeys = createRootWalletKeysFromV1Keychains(walletKeychains); + utxolib.bitgo.addXpubsToPsbt(psbt, rootWalletKeys); + + // Add multisig inputs with PSBT metadata + for (const unspent of unspents) { + const { chain, index } = parseChainPath(unspent.chainPath) as { chain: utxolib.bitgo.ChainCode; index: number }; + + const walletUnspent: utxolib.bitgo.WalletUnspent = { + id: `${unspent.tx_hash}:${unspent.tx_output_n}`, + address: unspent.address, + chain, + index, + value: BigInt(unspent.value), + }; + + utxolib.bitgo.addWalletUnspentToPsbt(psbt, walletUnspent, rootWalletKeys, 'user', 'backup', { + skipNonWitnessUtxo: true, + }); + } + + // Fee single key inputs are not supported with PSBT yet - throw to trigger fallback to legacy + if (feeSingleKeyUnspentsUsed.length > 0) { + throw new Error('PSBT does not support feeSingleKey inputs - use legacy transaction format'); + } + + // Add outputs (recipients, change, fees, OP_RETURNs) - already calculated in outputs array + for (const output of outputs) { + psbt.addOutput({ + script: (output as ScriptOutput).script, + value: BigInt(output.amount), + }); + } + + return psbt; + }; + // Serialize the transaction, returning what is needed to sign it const serialize = function () { + // Build and return PSBT format when usePsbt is explicitly true + // PSBT hex is returned in transactionHex field for backward compatibility + // Use utxolib.bitgo.isPsbt() to detect if transactionHex contains PSBT or legacy tx + if (params.usePsbt === true) { + const psbt = buildPsbt(); + return { + transactionHex: psbt.toHex(), + fee: fee, + changeAddresses: changeOutputs.map(function (co) { + return _.pick(co, ['address', 'path', 'amount']); + }), + walletId: params.wallet.id(), + feeRate: feeRate, + instant: params.instant, + bitgoFee: bitgoFeeInfo, + estimatedSize: minerFeeInfo.size, + travelInfos: travelInfos, + }; + } + + // Legacy format: return transactionHex with separate unspents array // only need to return the unspents that were used and just the chainPath, redeemScript, and instant flag const pickedUnspents: any = _.map(unspents, function (unspent) { return _.pick(unspent, ['chainPath', 'redeemScript', 'instant', 'witnessScript', 'script', 'value']); diff --git a/modules/sdk-api/src/v1/wallet.ts b/modules/sdk-api/src/v1/wallet.ts index 5144fe2b37..6e6bcba4a9 100644 --- a/modules/sdk-api/src/v1/wallet.ts +++ b/modules/sdk-api/src/v1/wallet.ts @@ -31,6 +31,25 @@ import { tryPromise } from '../util'; const TransactionBuilder = require('./transactionBuilder'); const PendingApproval = require('./pendingapproval'); +// PSBT rollout: 10% on mainnet, 100% on testnet +const V1_PSBT_ROLLOUT_PERCENT = 10; + +function shouldUsePsbt(bitgo: any, explicitUsePsbt?: boolean): boolean { + // Explicit setting always wins + if (explicitUsePsbt !== undefined) { + return explicitUsePsbt; + } + + // Testnet: always PSBT + const network = common.Environments[bitgo.getEnv()]?.network; + if (network !== 'bitcoin') { + return true; + } + + // Mainnet: 10% rollout + return Math.random() * 100 < V1_PSBT_ROLLOUT_PERCENT; +} + const { getExternalChainCode, getInternalChainCode, isChainCode, scriptTypeForChain } = utxolib.bitgo; const request = require('superagent'); @@ -894,6 +913,34 @@ Wallet.prototype.createTransaction = function (params, callback) { params.validate = params.validate !== undefined ? params.validate : this.bitgo.getValidate(); params.wallet = this; + // Apply PSBT rollout logic (respects explicit usePsbt if set) + const wantsPsbt = shouldUsePsbt(this.bitgo, params.usePsbt); + + if (wantsPsbt) { + // Try PSBT first, fall back to legacy on failure + return TransactionBuilder.createTransaction({ ...params, usePsbt: true }) + .then((result: any) => { + result.psbtAttempt = { success: true }; + return result; + }) + .catch((psbtError: Error) => { + // PSBT failed - fall back to legacy and capture error for backend reporting + console.warn('PSBT transaction failed, falling back to legacy:', psbtError.message); + return TransactionBuilder.createTransaction({ ...params, usePsbt: false }).then((result: any) => { + result.psbtAttempt = { + success: false, + error: psbtError.message, + stack: psbtError.stack?.split('\n').slice(0, 5).join('\n'), // First 5 lines + }; + return result; + }); + }) + .then(callback) + .catch(callback); + } + + // Legacy path + params.usePsbt = false; return TransactionBuilder.createTransaction(params).then(callback).catch(callback); }; @@ -913,8 +960,15 @@ Wallet.prototype.createTransaction = function (params, callback) { Wallet.prototype.signTransaction = function (params, callback) { params = _.extend({}, params); - if (params.psbt) { - return tryPromise(() => signPsbtRequest(params)) + // Route to PSBT signing if params.psbt exists OR if transactionHex contains a PSBT + // Use utxolib.bitgo.isPsbt() to auto-detect PSBT format in transactionHex + if (params.psbt || (params.transactionHex && utxolib.bitgo.isPsbt(params.transactionHex))) { + const psbtHex = params.psbt || params.transactionHex; + return tryPromise(() => signPsbtRequest({ psbt: psbtHex, keychain: params.keychain })) + .then(function (result) { + // Return result with transactionHex containing the signed PSBT for consistency + return { tx: result.psbt, transactionHex: result.psbt }; + }) .then(callback) .catch(callback); } @@ -1602,6 +1656,7 @@ Wallet.prototype.accelerateTransaction = function accelerateTransaction(params, const changeAddress = await this.createAddress({ chain: changeChain }); // create the child tx and broadcast + // Use legacy format - PSBT rollout applies to user-facing createTransaction only // @ts-expect-error - no implicit this const tx = await this.createAndSignTransaction({ unspents: unspentsToUse, @@ -1618,6 +1673,7 @@ Wallet.prototype.accelerateTransaction = function accelerateTransaction(params, }, xprv: params.xprv, walletPassphrase: params.walletPassphrase, + usePsbt: false, }); // child fee rate must be in sat/kB, so we need to convert @@ -1672,6 +1728,7 @@ Wallet.prototype.createAndSignTransaction = function (params, callback) { } // @ts-expect-error - no implicit this + // Build transaction (legacy format by default, PSBT when usePsbt: true) const transaction = (await this.createTransaction(params)) as any; const fee = transaction.fee; const feeRate = transaction.feeRate; @@ -1704,6 +1761,7 @@ Wallet.prototype.createAndSignTransaction = function (params, callback) { transaction.feeSingleKeyWIF = params.feeSingleKeyWIF; // @ts-expect-error - no implicit this + // signTransaction auto-detects PSBT vs legacy from transactionHex const result = await this.signTransaction(transaction); return _.extend(result, { fee, @@ -1713,6 +1771,7 @@ Wallet.prototype.createAndSignTransaction = function (params, callback) { travelInfos, estimatedSize, unspents, + psbtAttempt: transaction.psbtAttempt, // Propagate PSBT attempt info for error reporting }); } .call(this) diff --git a/modules/sdk-api/test/unit/v1/wallet.ts b/modules/sdk-api/test/unit/v1/wallet.ts index 92ac1be455..84c24425b4 100644 --- a/modules/sdk-api/test/unit/v1/wallet.ts +++ b/modules/sdk-api/test/unit/v1/wallet.ts @@ -202,6 +202,7 @@ describe('Wallet Prototype Methods', function () { address: '', }, opReturns: { 'BitGo p2sh test': 1000 }, + usePsbt: false, })) as any; transaction.transactionHex.should.equal( '010000000144dea5cb05425f94976e887ccba5686a9a12a3f49710b021508d3d9cd8de16b80100000000ffffffff02e803000000000000116a0f426974476f2070327368207465737422a107000000000017a914d039cb3344294a5a384a5508a006444c420cbc118700000000' @@ -227,6 +228,266 @@ describe('Wallet Prototype Methods', function () { ); }); + it('p2sh PSBT produces same unsigned tx as legacy', async function () { + const p2shAddress = fakeProdWallet.generateAddress({ path: '/0/13', segwit: false }); + const unspent: any = { + addresses: ['2NCEDmmKNNnqKvnWw7pE3RLzuFe5aHHVy1X'], + value: '0.00504422', + value_int: 504422, + txid: 'b816ded89c3d8d5021b01097f4a3129a6a68a5cb7c886e97945f4205cba5de44', + n: 1, + script_pub_key: { + asm: 'OP_HASH160 d039cb3344294a5a384a5508a006444c420cbc11 OP_EQUAL', + hex: 'a914d039cb3344294a5a384a5508a006444c420cbc1187', + }, + req_sigs: 1, + type: 'scripthash', + confirmations: 9, + id: 61330229, + }; + _.extend(unspent, p2shAddress); + unspent.value = unspent.value_int; + unspent.tx_hash = unspent.txid; + unspent.tx_output_n = unspent.n; + unspent.script = unspent.outputScript; + + const txParams = { + changeAddress: p2shAddress.address, + unspents: [unspent], + recipients: {}, + noSplitChange: true, + forceChangeAtEnd: true, + feeRate: 10000, + bitgoFee: { + amount: 0, + address: '', + }, + opReturns: { 'BitGo p2sh test': 1000 }, + }; + + // Create legacy transaction + nock(bgUrl).post('/api/v1/billing/address').reply(200, { address: '2MswQjkvN6oWYdE7L2brJ5cAAMjPmG59oco' }); + const legacyTx = (await fakeProdWallet.createTransaction({ ...txParams, usePsbt: false })) as any; + + // Create PSBT transaction (explicitly) + nock(bgUrl).post('/api/v1/billing/address').reply(200, { address: '2MswQjkvN6oWYdE7L2brJ5cAAMjPmG59oco' }); + const psbtTx = (await fakeProdWallet.createTransaction({ ...txParams, usePsbt: true })) as any; + + // Extract unsigned tx from PSBT and compare + const psbt = utxolib.bitgo.createPsbtFromHex(psbtTx.transactionHex, utxolib.networks.bitcoin); + const unsignedTxFromPsbt = psbt.getUnsignedTx().toHex(); + + // The unsigned transaction hex should be identical + legacyTx.transactionHex.should.equal(unsignedTxFromPsbt); + + // Fees should also match + legacyTx.fee.should.equal(psbtTx.fee); + + // Clean up nock mocks to avoid interference with other tests + nock.cleanAll(); + }); + + it('segwit PSBT produces same unsigned tx as legacy', async function () { + const segwitAddress = fakeProdWallet.generateAddress({ path: '/10/13', segwit: true }); + const unspent: any = { + addresses: ['2MxKkH8yB3S9YWmTQRbvmborYQyQnH5petP'], + value: '0.18750000', + value_int: 18750000, + txid: '7d282878a85daee5d46e043827daed57596d75d1aa6e04fd0c09a36f9130881f', + n: 0, + script_pub_key: { + asm: 'OP_HASH160 37b393fce627a0ec634eb543dda1e608e2d1c78a OP_EQUAL', + hex: 'a91437b393fce627a0ec634eb543dda1e608e2d1c78a87', + }, + req_sigs: 1, + type: 'scripthash', + confirmations: 0, + id: 61331617, + }; + _.extend(unspent, segwitAddress); + unspent.value = unspent.value_int; + unspent.tx_hash = unspent.txid; + unspent.tx_output_n = unspent.n; + unspent.script = unspent.outputScript; + + const txParams = { + changeAddress: segwitAddress.address, + unspents: [unspent], + recipients: {}, + noSplitChange: true, + forceChangeAtEnd: true, + feeRate: 10000, + bitgoFee: { + amount: 0, + address: '', + }, + opReturns: { 'BitGo segwit test': 1000 }, + }; + + // Create legacy transaction + nock(bgUrl).post('/api/v1/billing/address').reply(200, { address: '2MswQjkvN6oWYdE7L2brJ5cAAMjPmG59oco' }); + const legacyTx = (await fakeProdWallet.createTransaction({ ...txParams, usePsbt: false })) as any; + + // Create PSBT transaction (explicitly) + nock(bgUrl).post('/api/v1/billing/address').reply(200, { address: '2MswQjkvN6oWYdE7L2brJ5cAAMjPmG59oco' }); + const psbtTx = (await fakeProdWallet.createTransaction({ ...txParams, usePsbt: true })) as any; + + // Extract unsigned tx from PSBT and compare + const psbt = utxolib.bitgo.createPsbtFromHex(psbtTx.transactionHex, utxolib.networks.bitcoin); + const unsignedTxFromPsbt = psbt.getUnsignedTx().toHex(); + + // The unsigned transaction hex should be identical + legacyTx.transactionHex.should.equal(unsignedTxFromPsbt); + + // Fees should also match + legacyTx.fee.should.equal(psbtTx.fee); + + // Clean up nock mocks to avoid interference with other tests + nock.cleanAll(); + }); + + it('p2sh with PSBT format', async function () { + const p2shAddress = fakeProdWallet.generateAddress({ path: '/0/13', segwit: false }); + const unspent: any = { + addresses: ['2NCEDmmKNNnqKvnWw7pE3RLzuFe5aHHVy1X'], + value: '0.00504422', + value_int: 504422, + txid: 'b816ded89c3d8d5021b01097f4a3129a6a68a5cb7c886e97945f4205cba5de44', + n: 1, + script_pub_key: { + asm: 'OP_HASH160 d039cb3344294a5a384a5508a006444c420cbc11 OP_EQUAL', + hex: 'a914d039cb3344294a5a384a5508a006444c420cbc1187', + }, + req_sigs: 1, + type: 'scripthash', + confirmations: 9, + id: 61330229, + }; + _.extend(unspent, p2shAddress); + unspent.value = unspent.value_int; + unspent.tx_hash = unspent.txid; + unspent.tx_output_n = unspent.n; + unspent.script = unspent.outputScript; + + nock(bgUrl).post('/api/v1/billing/address').reply(200, { address: '2MswQjkvN6oWYdE7L2brJ5cAAMjPmG59oco' }); + + const transaction = (await fakeProdWallet.createTransaction({ + changeAddress: p2shAddress.address, + unspents: [unspent], + recipients: {}, + noSplitChange: true, + forceChangeAtEnd: true, + feeRate: 10000, + bitgoFee: { + amount: 0, + address: '', + }, + opReturns: { 'BitGo p2sh test': 1000 }, + usePsbt: true, // Explicitly request PSBT format + })) as any; + + // Verify PSBT format is returned in transactionHex + should.exist(transaction.transactionHex); + should.ok(utxolib.bitgo.isPsbt(transaction.transactionHex)); + should.not.exist(transaction.unspents); + + // Parse and validate the PSBT + const psbt = utxolib.bitgo.createPsbtFromHex(transaction.transactionHex, utxolib.networks.bitcoin); + psbt.data.inputs.length.should.equal(1); + psbt.data.outputs.length.should.equal(2); // OP_RETURN + change + + // Verify input has required PSBT metadata + const input = psbt.data.inputs[0]; + should.exist(input.witnessUtxo); + should.exist(input.redeemScript); + should.exist(input.bip32Derivation); + input.bip32Derivation!.length.should.equal(3); // user, backup, bitgo + + // Verify globalXpubs are set + should.exist(psbt.data.globalMap.globalXpub); + psbt.data.globalMap.globalXpub!.length.should.equal(3); + + // Sign with user key - auto-detects PSBT in transactionHex + const signedResult = (await fakeProdWallet.signTransaction({ + transactionHex: transaction.transactionHex, + keychain: userKeypair, + })) as any; + + should.exist(signedResult.tx); + + // Parse signed PSBT and verify signature + const signedPsbt = utxolib.bitgo.createPsbtFromHex(signedResult.tx, utxolib.networks.bitcoin); + should.ok(signedPsbt.validateSignaturesOfInputHD(0, utxolib.bip32.fromBase58(userKeypair.xpub))); + }); + + it('segwit with PSBT format', async function () { + const segwitAddress = fakeProdWallet.generateAddress({ path: '/10/13', segwit: true }); + const unspent: any = { + addresses: ['2MxKkH8yB3S9YWmTQRbvmborYQyQnH5petP'], + value: '0.18750000', + value_int: 18750000, + txid: '7d282878a85daee5d46e043827daed57596d75d1aa6e04fd0c09a36f9130881f', + n: 0, + script_pub_key: { + asm: 'OP_HASH160 37b393fce627a0ec634eb543dda1e608e2d1c78a OP_EQUAL', + hex: 'a91437b393fce627a0ec634eb543dda1e608e2d1c78a87', + }, + req_sigs: 1, + type: 'scripthash', + confirmations: 0, + id: 61331617, + }; + _.extend(unspent, segwitAddress); + unspent.value = unspent.value_int; + unspent.tx_hash = unspent.txid; + unspent.tx_output_n = unspent.n; + unspent.script = unspent.outputScript; + + nock(bgUrl).post('/api/v1/billing/address').reply(200, { address: '2MswQjkvN6oWYdE7L2brJ5cAAMjPmG59oco' }); + + const transaction = (await fakeProdWallet.createTransaction({ + changeAddress: segwitAddress.address, + unspents: [unspent], + recipients: {}, + noSplitChange: true, + forceChangeAtEnd: true, + feeRate: 10000, + bitgoFee: { + amount: 0, + address: '', + }, + opReturns: { 'BitGo segwit test': 1000 }, + usePsbt: true, // Explicitly request PSBT format + })) as any; + + // Verify PSBT format is returned in transactionHex + should.exist(transaction.transactionHex); + should.ok(utxolib.bitgo.isPsbt(transaction.transactionHex)); + + // Parse and validate the PSBT + const psbt = utxolib.bitgo.createPsbtFromHex(transaction.transactionHex, utxolib.networks.bitcoin); + psbt.data.inputs.length.should.equal(1); + + // Verify input has segwit PSBT metadata + const input = psbt.data.inputs[0]; + should.exist(input.witnessUtxo); + should.exist(input.witnessScript); + should.exist(input.redeemScript); + should.exist(input.bip32Derivation); + + // Sign with user key - auto-detects PSBT in transactionHex + const signedResult = (await fakeProdWallet.signTransaction({ + transactionHex: transaction.transactionHex, + keychain: userKeypair, + })) as any; + + should.exist(signedResult.tx); + + // Parse signed PSBT and verify signature + const signedPsbt = utxolib.bitgo.createPsbtFromHex(signedResult.tx, utxolib.networks.bitcoin); + should.ok(signedPsbt.validateSignaturesOfInputHD(0, utxolib.bip32.fromBase58(userKeypair.xpub))); + }); + it('BCH p2sh', async function () { const p2shAddress = fakeProdWallet.generateAddress({ path: '/0/13', segwit: false }); const unspent: any = { @@ -264,6 +525,7 @@ describe('Wallet Prototype Methods', function () { address: '', }, opReturns: { 'BitGo p2sh test': 1000 }, + usePsbt: false, })) as any; transaction.transactionHex.should.equal( '010000000144dea5cb05425f94976e887ccba5686a9a12a3f49710b021508d3d9cd8de16b80100000000ffffffff02e803000000000000116a0f426974476f2070327368207465737422a107000000000017a914d039cb3344294a5a384a5508a006444c420cbc118700000000' @@ -326,6 +588,7 @@ describe('Wallet Prototype Methods', function () { address: '', }, opReturns: { 'BitGo segwit test': 1000 }, + usePsbt: false, })) as any; transaction.transactionHex.should.equal( '01000000011f8830916fa3090cfd046eaad1756d5957edda2738046ed4e5ae5da87828287d0000000000ffffffff02e803000000000000136a11426974476f2073656777697420746573740e0f1e010000000017a91437b393fce627a0ec634eb543dda1e608e2d1c78a8700000000' @@ -394,6 +657,7 @@ describe('Wallet Prototype Methods', function () { amount: 0, address: '', }, + usePsbt: false, }); scope.isDone().should.be.true(); @@ -484,6 +748,7 @@ describe('Wallet Prototype Methods', function () { address: '', }, opReturns: { 'BitGo segwit test': 1000 }, + usePsbt: false, })) as any; transaction.transactionHex.should.equal( '01000000011f8830916fa3090cfd046eaad1756d5957edda2738046ed4e5ae5da87828287d0000000000ffffffff02e803000000000000136a11426974476f2073656777697420746573740e0f1e010000000017a91437b393fce627a0ec634eb543dda1e608e2d1c78a8700000000' @@ -551,6 +816,7 @@ describe('Wallet Prototype Methods', function () { amount: 81760, address: '2ND7jQR5itjGTbh3DKgbpZWSY9ungDrwcwb', }, + usePsbt: false, })) as any; transaction.transactionHex.should.equal( '01000000027c75f8b4061212ec4669ef10c7a85a6bd8b677e74ecffef72df1e35b0ace54f60100000000ffffffff249f4f3b89110526e9d71f33679c5303dbf00ef43dac90b867ae2f043f9c40a40000000000ffffffff030084d71700000000206a1e426974476f206d6978656420703273682026207365677769742074657374b08ff9020000000017a9148153e7a35508088b6cf599226792c7de2dbff25287603f01000000000017a914d9f7be47975c036f94228b0bfd70701912758ba98700000000' @@ -632,6 +898,7 @@ describe('Wallet Prototype Methods', function () { amount: 81760, address: '2ND7jQR5itjGTbh3DKgbpZWSY9ungDrwcwb', }, + usePsbt: false, })) as any; transaction.transactionHex.should.equal( '01000000027c75f8b4061212ec4669ef10c7a85a6bd8b677e74ecffef72df1e35b0ace54f60100000000ffffffff249f4f3b89110526e9d71f33679c5303dbf00ef43dac90b867ae2f043f9c40a40000000000ffffffff04e09304000000000022002047044b55dab740b0c302853b27b8e3f50a79023aca367c94ee006f11bb79368f0084d71700000000206a1e426974476f206d69786564207032736820262073656777697420746573747cfaf4020000000017a9148153e7a35508088b6cf599226792c7de2dbff25287603f01000000000017a914d9f7be47975c036f94228b0bfd70701912758ba98700000000'