Skip to content

Commit 88e3135

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): always use wasm-utxo backend for recovery
Remove utxolib PSBT backend option and always use wasm-utxo for all recoveries. This simplifies the code and ensures consistency across all environments. Issue: BTC-2891 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 7cd76fc commit 88e3135

112 files changed

Lines changed: 301 additions & 464 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/abstract-utxo/src/recovery/backupKeyRecovery.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction';
1818
import { generateAddressWithChainAndIndex } from '../address';
1919
import { encodeTransaction } from '../transaction/decode';
2020
import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection';
21-
import { isTestnetCoin, UtxoCoinName } from '../names';
21+
import { UtxoCoinName } from '../names';
2222
import type { WalletUnspent } from '../unspent';
2323

2424
import { forCoin, RecoveryProvider } from './RecoveryProvider';
2525
import { MempoolApi } from './mempoolApi';
2626
import { CoingeckoApi } from './coingeckoApi';
27-
import { createBackupKeyRecoveryPsbt, getRecoveryAmount, PsbtBackend, toPsbtToUtxolibPsbt } from './psbt';
27+
import { createBackupKeyRecoveryPsbt, getRecoveryAmount, toPsbtToUtxolibPsbt } from './psbt';
2828

2929
type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
3030
type ChainCode = utxolib.bitgo.ChainCode;
@@ -370,20 +370,12 @@ export async function backupKeyRecovery(
370370
}
371371
}
372372

373-
// Use wasm-utxo for testnet coins only, utxolib for mainnet
374-
const backend: PsbtBackend = isTestnetCoin(coin.name) ? 'wasm-utxo' : 'utxolib';
375-
let psbt = createBackupKeyRecoveryPsbt(
376-
coin.getChain(),
377-
walletKeys,
378-
unspents,
379-
{
380-
feeRateSatVB: feePerByte,
381-
recoveryDestination: params.recoveryDestination,
382-
keyRecoveryServiceFee: krsFee,
383-
keyRecoveryServiceFeeAddress: krsFeeAddress,
384-
},
385-
backend
386-
);
373+
let psbt = createBackupKeyRecoveryPsbt(coin.getChain(), walletKeys, unspents, {
374+
feeRateSatVB: feePerByte,
375+
recoveryDestination: params.recoveryDestination,
376+
keyRecoveryServiceFee: krsFee,
377+
keyRecoveryServiceFeeAddress: krsFeeAddress,
378+
});
387379

388380
if (isUnsignedSweep) {
389381
return {
@@ -414,13 +406,7 @@ export async function backupKeyRecovery(
414406
psbt = signAndVerifyPsbt(psbt, walletKeys.backup, rootWalletKeysWasm, replayProtection);
415407
// Finalize and extract transaction
416408
psbt.finalizeAllInputs();
417-
if (psbt instanceof utxolib.bitgo.UtxoPsbt) {
418-
txInfo.transactionHex = psbt.extractTransaction().toBuffer().toString('hex');
419-
} else if (psbt instanceof fixedScriptWallet.BitGoPsbt) {
420-
txInfo.transactionHex = Buffer.from(psbt.extractTransaction().toBytes()).toString('hex');
421-
} else {
422-
throw new Error('expected a UtxoPsbt or BitGoPsbt object');
423-
}
409+
txInfo.transactionHex = Buffer.from(psbt.extractTransaction().toBytes()).toString('hex');
424410
}
425411

426412
if (isKrsRecovery) {

modules/abstract-utxo/src/recovery/crossChainRecovery.ts

Lines changed: 5 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
import * as utxolib from '@bitgo/utxo-lib';
22
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
3-
import { Dimensions } from '@bitgo/unspents';
43
import { CoinName, fixedScriptWallet } from '@bitgo/wasm-utxo';
54
import { BitGoBase, IWallet, Keychain, Triple, Wallet } from '@bitgo/sdk-core';
65
import { decrypt } from '@bitgo/sdk-api';
76

87
import { AbstractUtxoCoin, TransactionInfo } from '../abstractUtxoCoin';
98
import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction';
10-
import { getNetworkFromCoinName, isTestnetCoin, UtxoCoinName } from '../names';
9+
import { getNetworkFromCoinName, UtxoCoinName } from '../names';
1110
import { encodeTransaction } from '../transaction/decode';
1211
import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection';
1312
import { toTNumber } from '../tnumber';
1413
import type { Unspent, WalletUnspent } from '../unspent';
1514

16-
import {
17-
PsbtBackend,
18-
createEmptyWasmPsbt,
19-
addWalletInputsToWasmPsbt,
20-
addOutputToWasmPsbt,
21-
getRecoveryAmount,
22-
} from './psbt';
15+
import { createEmptyWasmPsbt, addWalletInputsToWasmPsbt, addOutputToWasmPsbt, getRecoveryAmount } from './psbt';
2316

2417
const { unspentSum } = utxolib.bitgo;
2518
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
@@ -337,66 +330,16 @@ async function getPrv(xprv?: string, passphrase?: string, wallet?: IWallet | Wal
337330
return getPrv(decrypt(passphrase, encryptedPrv));
338331
}
339332

340-
/**
341-
* Create a sweep transaction for cross-chain recovery using PSBT (utxolib implementation)
342-
* @param network
343-
* @param walletKeys
344-
* @param unspents
345-
* @param targetAddress
346-
* @param feeRateSatVB
347-
* @return unsigned PSBT
348-
*/
349-
function createSweepTransactionUtxolib<TNumber extends number | bigint = number>(
350-
coinName: CoinName,
351-
walletKeys: RootWalletKeys,
352-
unspents: WalletUnspent<TNumber>[],
353-
targetAddress: string,
354-
feeRateSatVB: number
355-
): utxolib.bitgo.UtxoPsbt {
356-
const network = getNetworkFromCoinName(coinName);
357-
const inputValue = unspentSum<bigint>(
358-
unspents.map((u) => ({ ...u, value: BigInt(u.value) })),
359-
'bigint'
360-
);
361-
const vsize = Dimensions.fromUnspents(unspents, {
362-
p2tr: { scriptPathLevel: 1 },
363-
p2trMusig2: { scriptPathLevel: undefined },
364-
})
365-
.plus(Dimensions.fromOutput({ script: utxolib.address.toOutputScript(targetAddress, network) }))
366-
.getVSize();
367-
const fee = BigInt(Math.round(vsize * feeRateSatVB));
368-
369-
const psbt = utxolib.bitgo.createPsbtForNetwork({ network });
370-
utxolib.bitgo.addXpubsToPsbt(psbt, walletKeys);
371-
372-
unspents.forEach((unspent) => {
373-
utxolib.bitgo.addWalletUnspentToPsbt(
374-
psbt,
375-
{ ...unspent, value: BigInt(unspent.value) },
376-
walletKeys,
377-
'user',
378-
'backup',
379-
{ skipNonWitnessUtxo: true }
380-
);
381-
});
382-
383-
const recoveryOutputScript = utxolib.address.toOutputScript(targetAddress, network);
384-
psbt.addOutput({ script: recoveryOutputScript, value: inputValue - fee });
385-
386-
return psbt;
387-
}
388-
389333
/**
390334
* Create a sweep transaction for cross-chain recovery using wasm-utxo
391-
* @param network
335+
* @param coinName - BitGo coin name (e.g. 'btc', 'tbtc', 'ltc')
392336
* @param walletKeys
393337
* @param unspents
394338
* @param targetAddress
395339
* @param feeRateSatVB
396-
* @param coinName - BitGo coin name (e.g. 'btc', 'tbtc', 'ltc')
397340
* @return unsigned PSBT
398341
*/
399-
function createSweepTransactionWasm<TNumber extends number | bigint = number>(
342+
function createSweepTransaction<TNumber extends number | bigint = number>(
400343
coinName: CoinName,
401344
walletKeys: RootWalletKeys,
402345
unspents: WalletUnspent<TNumber>[],
@@ -422,36 +365,9 @@ function createSweepTransactionWasm<TNumber extends number | bigint = number>(
422365
// Add output to wasm PSBT
423366
addOutputToWasmPsbt(wasmPsbt, targetAddress, inputValue - fee, coinName);
424367

425-
// Convert to utxolib PSBT for signing and return
426368
return wasmPsbt;
427369
}
428370

429-
/**
430-
* Create a sweep transaction for cross-chain recovery using PSBT
431-
* @param network
432-
* @param walletKeys
433-
* @param unspents
434-
* @param targetAddress
435-
* @param feeRateSatVB
436-
* @param backend - Which backend to use for PSBT creation (default: 'wasm-utxo')
437-
* @param coinName - BitGo coin name (required for wasm-utxo backend)
438-
* @return unsigned PSBT
439-
*/
440-
function createSweepTransaction<TNumber extends number | bigint = number>(
441-
coinName: CoinName,
442-
walletKeys: RootWalletKeys,
443-
unspents: WalletUnspent<TNumber>[],
444-
targetAddress: string,
445-
feeRateSatVB: number,
446-
backend: PsbtBackend = 'wasm-utxo'
447-
): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt {
448-
if (backend === 'wasm-utxo') {
449-
return createSweepTransactionWasm(coinName, walletKeys, unspents, targetAddress, feeRateSatVB);
450-
} else {
451-
return createSweepTransactionUtxolib(coinName, walletKeys, unspents, targetAddress, feeRateSatVB);
452-
}
453-
}
454-
455371
type RecoverParams = {
456372
/** Wallet ID (can be v1 wallet or v2 wallet) */
457373
walletId: string;
@@ -500,15 +416,12 @@ export async function recoverCrossChain<TNumber extends number | bigint = number
500416
const feeRateSatVB = await getFeeRateSatVB(params.sourceCoin);
501417

502418
// Create PSBT for both signed and unsigned recovery
503-
// Use wasm-utxo for testnet coins only, utxolib for mainnet
504-
const backend: PsbtBackend = isTestnetCoin(params.sourceCoin.name) ? 'wasm-utxo' : 'utxolib';
505419
let psbt = createSweepTransaction<TNumber>(
506420
params.sourceCoin.getChain(),
507421
walletKeys,
508422
walletUnspents,
509423
params.recoveryAddress,
510-
feeRateSatVB,
511-
backend
424+
feeRateSatVB
512425
);
513426

514427
// For unsigned recovery, return unsigned PSBT hex

modules/abstract-utxo/src/recovery/psbt.ts

Lines changed: 12 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as utxolib from '@bitgo/utxo-lib';
2-
import { Dimensions } from '@bitgo/unspents';
3-
import { CoinName, fixedScriptWallet, utxolibCompat, address as wasmAddress } from '@bitgo/wasm-utxo';
2+
import { CoinName, fixedScriptWallet, address as wasmAddress } from '@bitgo/wasm-utxo';
43

5-
import { getNetworkFromCoinName, UtxoCoinName } from '../names';
4+
import { getNetworkFromCoinName } from '../names';
65
import type { WalletUnspent } from '../unspent';
76

87
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
@@ -11,13 +10,6 @@ const { chainCodesP2tr, chainCodesP2trMusig2 } = utxolib.bitgo;
1110

1211
type ChainCode = utxolib.bitgo.ChainCode;
1312

14-
/**
15-
* Backend to use for PSBT creation.
16-
* - 'wasm-utxo': Use wasm-utxo for PSBT creation (default)
17-
* - 'utxolib': Use utxolib for PSBT creation (legacy)
18-
*/
19-
export type PsbtBackend = 'wasm-utxo' | 'utxolib';
20-
2113
/**
2214
* Check if a chain code is for a taproot script type
2315
*/
@@ -27,18 +19,6 @@ export function isTaprootChain(chain: ChainCode): boolean {
2719
);
2820
}
2921

30-
/**
31-
* Convert coin name to wasm-utxo network name
32-
*/
33-
export function toNetworkName(coinName: UtxoCoinName): utxolibCompat.UtxolibName {
34-
const network = getNetworkFromCoinName(coinName);
35-
const networkName = utxolib.getNetworkName(network);
36-
if (!networkName) {
37-
throw new Error(`Invalid coinName: ${coinName}`);
38-
}
39-
return networkName;
40-
}
41-
4222
class InsufficientFundsError extends Error {
4323
constructor(
4424
public totalInputAmount: bigint,
@@ -65,56 +45,6 @@ interface CreateBackupKeyRecoveryPsbtOptions {
6545
blockHeight?: number;
6646
}
6747

68-
/**
69-
* Create a backup key recovery PSBT using utxolib (legacy implementation)
70-
*/
71-
function createBackupKeyRecoveryPsbtUtxolib(
72-
coinName: CoinName,
73-
rootWalletKeys: RootWalletKeys,
74-
unspents: WalletUnspent<bigint>[],
75-
options: CreateBackupKeyRecoveryPsbtOptions
76-
): utxolib.bitgo.UtxoPsbt {
77-
const network = getNetworkFromCoinName(coinName);
78-
const { feeRateSatVB, recoveryDestination, keyRecoveryServiceFee, keyRecoveryServiceFeeAddress } = options;
79-
80-
const psbt = utxolib.bitgo.createPsbtForNetwork({ network });
81-
utxolib.bitgo.addXpubsToPsbt(psbt, rootWalletKeys);
82-
unspents.forEach((unspent) => {
83-
utxolib.bitgo.addWalletUnspentToPsbt(psbt, unspent, rootWalletKeys, 'user', 'backup');
84-
});
85-
86-
let dimensions = Dimensions.fromPsbt(psbt).plus(
87-
Dimensions.fromOutput({ script: utxolib.address.toOutputScript(recoveryDestination, network) })
88-
);
89-
90-
if (keyRecoveryServiceFeeAddress) {
91-
dimensions = dimensions.plus(
92-
Dimensions.fromOutput({
93-
script: utxolib.address.toOutputScript(keyRecoveryServiceFeeAddress, network),
94-
})
95-
);
96-
}
97-
98-
const approximateFee = BigInt(dimensions.getVSize() * feeRateSatVB);
99-
const totalInputAmount = utxolib.bitgo.unspentSum(unspents, 'bigint');
100-
const recoveryAmount = totalInputAmount - approximateFee - keyRecoveryServiceFee;
101-
102-
if (recoveryAmount < BigInt(0)) {
103-
throw new InsufficientFundsError(totalInputAmount, approximateFee, keyRecoveryServiceFee, recoveryAmount);
104-
}
105-
106-
psbt.addOutput({ script: utxolib.address.toOutputScript(recoveryDestination, network), value: recoveryAmount });
107-
108-
if (keyRecoveryServiceFeeAddress) {
109-
psbt.addOutput({
110-
script: utxolib.address.toOutputScript(keyRecoveryServiceFeeAddress, network),
111-
value: keyRecoveryServiceFee,
112-
});
113-
}
114-
115-
return psbt;
116-
}
117-
11848
/**
11949
* Check if the network is a Zcash network
12050
*/
@@ -279,57 +209,39 @@ function createBackupKeyRecoveryPsbtWasm(
279209
addOutputToWasmPsbt(wasmPsbt, keyRecoveryServiceFeeAddress, keyRecoveryServiceFee, coinName);
280210
}
281211

282-
// Convert to utxolib PSBT for signing and return
283212
return wasmPsbt;
284213
}
285214

286215
/**
287216
* Create a backup key recovery PSBT.
288217
*
289-
* @param network - The network for the PSBT
218+
* @param coinName - The coin name for the PSBT
290219
* @param rootWalletKeys - The wallet keys
291220
* @param unspents - The unspents to include in the PSBT
292221
* @param options - Options for creating the PSBT
293-
* @param backend - Which backend to use for PSBT creation (default: 'wasm-utxo')
294222
*/
295223
export function createBackupKeyRecoveryPsbt(
296224
coinName: CoinName,
297225
rootWalletKeys: RootWalletKeys,
298226
unspents: WalletUnspent<bigint>[],
299-
options: CreateBackupKeyRecoveryPsbtOptions,
300-
backend: PsbtBackend = 'wasm-utxo'
301-
): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt {
227+
options: CreateBackupKeyRecoveryPsbtOptions
228+
): fixedScriptWallet.BitGoPsbt {
302229
if (options.keyRecoveryServiceFee > 0 && !options.keyRecoveryServiceFeeAddress) {
303230
throw new Error('keyRecoveryServiceFeeAddress is required when keyRecoveryServiceFee is provided');
304231
}
305232

306-
if (backend === 'wasm-utxo') {
307-
return createBackupKeyRecoveryPsbtWasm(coinName, rootWalletKeys, unspents, options);
308-
} else {
309-
return createBackupKeyRecoveryPsbtUtxolib(coinName, rootWalletKeys, unspents, options);
310-
}
233+
return createBackupKeyRecoveryPsbtWasm(coinName, rootWalletKeys, unspents, options);
311234
}
312235

313236
export function getRecoveryAmount(
314-
psbt: utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt,
237+
psbt: fixedScriptWallet.BitGoPsbt,
315238
walletKeys: RootWalletKeys,
316239
address: string
317240
): bigint {
318-
if (psbt instanceof utxolib.bitgo.UtxoPsbt) {
319-
const recoveryOutputScript = utxolib.address.toOutputScript(address, psbt.network);
320-
const output = psbt.txOutputs.find((o) => o.script.equals(recoveryOutputScript));
321-
if (!output) {
322-
throw new Error(`Recovery destination output not found in PSBT: ${address}`);
323-
}
324-
return output.value;
325-
}
326-
if (psbt instanceof fixedScriptWallet.BitGoPsbt) {
327-
const parsedOutputs = psbt.parseOutputsWithWalletKeys(walletKeys);
328-
const recoveryOutput = parsedOutputs.find((o) => o.address === address);
329-
if (!recoveryOutput) {
330-
throw new Error(`Recovery destination output not found in PSBT: ${address}`);
331-
}
332-
return recoveryOutput.value;
241+
const parsedOutputs = psbt.parseOutputsWithWalletKeys(walletKeys);
242+
const recoveryOutput = parsedOutputs.find((o) => o.address === address);
243+
if (!recoveryOutput) {
244+
throw new Error(`Recovery destination output not found in PSBT: ${address}`);
333245
}
334-
throw new Error('Invalid PSBT type');
246+
return BigInt(recoveryOutput.value);
335247
}

modules/abstract-utxo/test/unit/fixtures/bch/recovery/backupKeyRecovery-fullSignedRecovery-customUserKeyPath-p2sh.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@
2525
"valueString": "300000000"
2626
}
2727
],
28-
"transactionHex": "02000000038bb70cc33ba011c71d725c686a81b1f19d2955d204300f384838725f076edd9800000000fdfe0000483045022100e68d7057fdad1fd9e7da1dd0d2745600cd7ebc6b3bdfdc8c977c27f117dec1ee022014a862be7e83b092cea8c4791d47d9ea87bc3a7e4d7851fad30e9da0a8933efc41483045022100d4295855382edd094687ade706ccf51375c716e3acd2156cb0d7403f857a795f0220409c5b8f8ed66f43e563c2c4e401b8ca0cfab3c89452645c92c4010ee07d74d5414c69522102587d7749d1ed2a3d3d300b969e68cbedb042ea19e3ee90c4131e2092f5e1181e21030795af84ecc10252d8a894f54845beeb5624a1c24c3747cc654bd430539dee3521029b30ebe8eb23f8cec82f25a80e3b423979ec3ba1fe07d9d4ed9f6361258bc31d53aeffffffffeec68db14dcb1f1340f46c6f9d9a7303915ccd9022d59e7c9e13dae7c91ffe9b00000000fc004730440220487d165adcc526d5bf659e5dfec94e07c8eaa6567308d29a7b4676456e71288802204172d68f63bcc29141095b81a9366056b6d11260d86c6f1dfa8a154953b0a7854147304402205d3c5b6105a2fa1819973ef6b83c1575468be0bce6757992b365583c11690fa902200134cc5b58d6590664f45e797990334b4fa989b21ef2ec5194a9d3ae262855ad414c69522102587d7749d1ed2a3d3d300b969e68cbedb042ea19e3ee90c4131e2092f5e1181e21030795af84ecc10252d8a894f54845beeb5624a1c24c3747cc654bd430539dee3521029b30ebe8eb23f8cec82f25a80e3b423979ec3ba1fe07d9d4ed9f6361258bc31d53aeffffffffe3ad05836997543b105275534081e8eac637e0e4d2a5bc544d7b0d22f32d49de00000000fc00473044022025d60881a0bf878533362094e8a531f1a066fa2f85ac92d5965f20d7227682c20220685efc33bb4e3a81963f4ebd0a18ec088db96f20432e1c943228e2c1fff2996141473044022065fb4062083c3cbf12638cf087b36512d22458cdf76e5f92582992885efab050022039885486cc1b912d0843cf8227a7473b5938c8927a9b7f41efd03af87752fffe414c69522102587d7749d1ed2a3d3d300b969e68cbedb042ea19e3ee90c4131e2092f5e1181e21030795af84ecc10252d8a894f54845beeb5624a1c24c3747cc654bd430539dee3521029b30ebe8eb23f8cec82f25a80e3b423979ec3ba1fe07d9d4ed9f6361258bc31d53aeffffffff0160d8c1230000000017a91439c65a0d0072a140694d6b13ec5f5f2437de99ff8700000000",
29-
"txid": "b50d92e5be1c143941ad3ce4aa176c69c6299cd4c689d5caceeff5f943f8ddb3"
28+
"transactionHex": "02000000038bb70cc33ba011c71d725c686a81b1f19d2955d204300f384838725f076edd9800000000fdfd0000483045022100a8fdd2a692da1daba4cbf1915d3193dcc29ecae91525750cd84277aa5a0baa91022046cabd7460db3ac554ea35173c9077fe4286d78a0a4205bc91422d4b834e6fa14147304402200104623fc6c7dbe8c027fe71e677d3be2c7f73adf94d07d00cbe1bee96f6508102204a38a8fcaa50cb2028e95fa51dcd33058b077f548291a61a7ff6d6d56c5a5684414c69522102587d7749d1ed2a3d3d300b969e68cbedb042ea19e3ee90c4131e2092f5e1181e21030795af84ecc10252d8a894f54845beeb5624a1c24c3747cc654bd430539dee3521029b30ebe8eb23f8cec82f25a80e3b423979ec3ba1fe07d9d4ed9f6361258bc31d53aefeffffffeec68db14dcb1f1340f46c6f9d9a7303915ccd9022d59e7c9e13dae7c91ffe9b00000000fc0047304402200a223a6d23dcec4be36563f2fb9e6ae5c65db38961dd1fc33fa12c1f8f88c5a802204696d4ff62512c0262161efd751bb717b32ad97c83a8a692f7811f68d0c22a6741473044022023b2cb674c502085b9c351c5386c73c0a9e422ea4d08cd7b395cd374ad4b94d302201110faeaba5ea428769b182ceb0868efd09b60659a62e235a8f6155ca6e4bef2414c69522102587d7749d1ed2a3d3d300b969e68cbedb042ea19e3ee90c4131e2092f5e1181e21030795af84ecc10252d8a894f54845beeb5624a1c24c3747cc654bd430539dee3521029b30ebe8eb23f8cec82f25a80e3b423979ec3ba1fe07d9d4ed9f6361258bc31d53aefeffffffe3ad05836997543b105275534081e8eac637e0e4d2a5bc544d7b0d22f32d49de00000000fc0047304402200597e97b2a4134ca16ffe54d9edfe990078ebf8d06dce9fc50debf5fb6317c4802202c6f89584a4490e2a251f5a5c0a9287613f151bcea59ac9d87e36249c0b087d04147304402205a83d823d1d8c5633a452f18113073a9a2a96995d14dec3e1ad8e18cc2b01a8802206b2d6b28a1531148526f45ad04d5449271a70c1b21ab7141f9a76ef25035e18d414c69522102587d7749d1ed2a3d3d300b969e68cbedb042ea19e3ee90c4131e2092f5e1181e21030795af84ecc10252d8a894f54845beeb5624a1c24c3747cc654bd430539dee3521029b30ebe8eb23f8cec82f25a80e3b423979ec3ba1fe07d9d4ed9f6361258bc31d53aefeffffff0134d7c1230000000017a91439c65a0d0072a140694d6b13ec5f5f2437de99ff8700000000",
29+
"txid": "d3df089c810a80589df88c6b5a7df6df9ee4e29a50c391a8ca1aace7927ab72f"
3030
}

0 commit comments

Comments
 (0)