diff --git a/modules/sdk-api/src/v1/transactionBuilder.ts b/modules/sdk-api/src/v1/transactionBuilder.ts index 9bfca327af..be03a14d68 100644 --- a/modules/sdk-api/src/v1/transactionBuilder.ts +++ b/modules/sdk-api/src/v1/transactionBuilder.ts @@ -21,46 +21,6 @@ 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; @@ -275,9 +235,6 @@ 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. @@ -646,8 +603,7 @@ exports.createTransaction = function (params) { throw new Error('transaction too large: estimated size ' + minerFeeInfo.size + ' bytes'); } - // Reset outputs array (use outer scope variable) - outputs = []; + const outputs: Output[] = []; recipients.forEach(function (recipient) { let script; @@ -783,75 +739,8 @@ 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 b8be0b0903..5144fe2b37 100644 --- a/modules/sdk-api/src/v1/wallet.ts +++ b/modules/sdk-api/src/v1/wallet.ts @@ -31,25 +31,6 @@ import { tryPromise } from '../util'; const TransactionBuilder = require('./transactionBuilder'); const PendingApproval = require('./pendingapproval'); -// PSBT rollout: 0% on mainnet, 100% on testnet -const V1_PSBT_ROLLOUT_PERCENT = 0; - -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'); @@ -913,33 +894,6 @@ 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'); - return TransactionBuilder.createTransaction({ ...params, usePsbt: false }).then((result: any) => { - result.psbtAttempt = { - success: false, - stack: psbtError.stack?.split('\n').slice(0, 5).join('\n'), // First 5 lines only - }; - return result; - }); - }) - .then(callback) - .catch(callback); - } - - // Legacy path - params.usePsbt = false; return TransactionBuilder.createTransaction(params).then(callback).catch(callback); }; @@ -959,15 +913,8 @@ Wallet.prototype.createTransaction = function (params, callback) { Wallet.prototype.signTransaction = function (params, callback) { params = _.extend({}, 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 }; - }) + if (params.psbt) { + return tryPromise(() => signPsbtRequest(params)) .then(callback) .catch(callback); } @@ -1655,7 +1602,6 @@ 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, @@ -1672,7 +1618,6 @@ 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 @@ -1727,7 +1672,6 @@ 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; @@ -1760,7 +1704,6 @@ 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, @@ -1770,7 +1713,6 @@ 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 84c24425b4..92ac1be455 100644 --- a/modules/sdk-api/test/unit/v1/wallet.ts +++ b/modules/sdk-api/test/unit/v1/wallet.ts @@ -202,7 +202,6 @@ describe('Wallet Prototype Methods', function () { address: '', }, opReturns: { 'BitGo p2sh test': 1000 }, - usePsbt: false, })) as any; transaction.transactionHex.should.equal( '010000000144dea5cb05425f94976e887ccba5686a9a12a3f49710b021508d3d9cd8de16b80100000000ffffffff02e803000000000000116a0f426974476f2070327368207465737422a107000000000017a914d039cb3344294a5a384a5508a006444c420cbc118700000000' @@ -228,266 +227,6 @@ 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 = { @@ -525,7 +264,6 @@ describe('Wallet Prototype Methods', function () { address: '', }, opReturns: { 'BitGo p2sh test': 1000 }, - usePsbt: false, })) as any; transaction.transactionHex.should.equal( '010000000144dea5cb05425f94976e887ccba5686a9a12a3f49710b021508d3d9cd8de16b80100000000ffffffff02e803000000000000116a0f426974476f2070327368207465737422a107000000000017a914d039cb3344294a5a384a5508a006444c420cbc118700000000' @@ -588,7 +326,6 @@ describe('Wallet Prototype Methods', function () { address: '', }, opReturns: { 'BitGo segwit test': 1000 }, - usePsbt: false, })) as any; transaction.transactionHex.should.equal( '01000000011f8830916fa3090cfd046eaad1756d5957edda2738046ed4e5ae5da87828287d0000000000ffffffff02e803000000000000136a11426974476f2073656777697420746573740e0f1e010000000017a91437b393fce627a0ec634eb543dda1e608e2d1c78a8700000000' @@ -657,7 +394,6 @@ describe('Wallet Prototype Methods', function () { amount: 0, address: '', }, - usePsbt: false, }); scope.isDone().should.be.true(); @@ -748,7 +484,6 @@ describe('Wallet Prototype Methods', function () { address: '', }, opReturns: { 'BitGo segwit test': 1000 }, - usePsbt: false, })) as any; transaction.transactionHex.should.equal( '01000000011f8830916fa3090cfd046eaad1756d5957edda2738046ed4e5ae5da87828287d0000000000ffffffff02e803000000000000136a11426974476f2073656777697420746573740e0f1e010000000017a91437b393fce627a0ec634eb543dda1e608e2d1c78a8700000000' @@ -816,7 +551,6 @@ describe('Wallet Prototype Methods', function () { amount: 81760, address: '2ND7jQR5itjGTbh3DKgbpZWSY9ungDrwcwb', }, - usePsbt: false, })) as any; transaction.transactionHex.should.equal( '01000000027c75f8b4061212ec4669ef10c7a85a6bd8b677e74ecffef72df1e35b0ace54f60100000000ffffffff249f4f3b89110526e9d71f33679c5303dbf00ef43dac90b867ae2f043f9c40a40000000000ffffffff030084d71700000000206a1e426974476f206d6978656420703273682026207365677769742074657374b08ff9020000000017a9148153e7a35508088b6cf599226792c7de2dbff25287603f01000000000017a914d9f7be47975c036f94228b0bfd70701912758ba98700000000' @@ -898,7 +632,6 @@ describe('Wallet Prototype Methods', function () { amount: 81760, address: '2ND7jQR5itjGTbh3DKgbpZWSY9ungDrwcwb', }, - usePsbt: false, })) as any; transaction.transactionHex.should.equal( '01000000027c75f8b4061212ec4669ef10c7a85a6bd8b677e74ecffef72df1e35b0ace54f60100000000ffffffff249f4f3b89110526e9d71f33679c5303dbf00ef43dac90b867ae2f043f9c40a40000000000ffffffff04e09304000000000022002047044b55dab740b0c302853b27b8e3f50a79023aca367c94ee006f11bb79368f0084d71700000000206a1e426974476f206d69786564207032736820262073656777697420746573747cfaf4020000000017a9148153e7a35508088b6cf599226792c7de2dbff25287603f01000000000017a914d9f7be47975c036f94228b0bfd70701912758ba98700000000'