Skip to content

Commit 4e53e5a

Browse files
Merge pull request #7864 from BitGo/BTC-2909-dims.use-coinname
feat(abstract-utxo): refactor PSBT handling and implement wasm-utxo support
2 parents ed1b256 + 6754c34 commit 4e53e5a

File tree

128 files changed

+326
-277
lines changed

Some content is hidden

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

128 files changed

+326
-277
lines changed

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

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@ import {
1111
krsProviders,
1212
} from '@bitgo/sdk-core';
1313
import { getMainnet, networks } from '@bitgo/utxo-lib';
14-
import { CoinName } from '@bitgo/wasm-utxo';
14+
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
1515

1616
import { AbstractUtxoCoin } from '../abstractUtxoCoin';
17-
import { signAndVerifyPsbt } from '../transaction/fixedScript/signPsbt';
17+
import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction';
1818
import { generateAddressWithChainAndIndex } from '../address';
19+
import { encodeTransaction } from '../transaction/decode';
20+
import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection';
1921

2022
import { forCoin, RecoveryProvider } from './RecoveryProvider';
2123
import { MempoolApi } from './mempoolApi';
2224
import { CoingeckoApi } from './coingeckoApi';
23-
import { createBackupKeyRecoveryPsbt, getRecoveryAmount, PsbtBackend } from './psbt';
25+
import { createBackupKeyRecoveryPsbt, getRecoveryAmount, PsbtBackend, toPsbtToUtxolibPsbt } from './psbt';
2426

2527
type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
2628
type ChainCode = utxolib.bitgo.ChainCode;
@@ -369,48 +371,61 @@ export async function backupKeyRecovery(
369371

370372
// Use wasm-utxo for testnet coins only, utxolib for mainnet
371373
const backend: PsbtBackend = utxolib.isTestnet(coin.network) ? 'wasm-utxo' : 'utxolib';
372-
const psbt = createBackupKeyRecoveryPsbt(
373-
coin.network,
374+
let psbt = createBackupKeyRecoveryPsbt(
375+
coin.getChain(),
374376
walletKeys,
375377
unspents,
376378
{
377379
feeRateSatVB: feePerByte,
378380
recoveryDestination: params.recoveryDestination,
379381
keyRecoveryServiceFee: krsFee,
380382
keyRecoveryServiceFeeAddress: krsFeeAddress,
381-
coinName: coin.getChain() as CoinName,
382383
},
383384
backend
384385
);
385386

386387
if (isUnsignedSweep) {
387388
return {
388-
txHex: psbt.toHex(),
389+
txHex: encodeTransaction(psbt).toString('hex'),
389390
txInfo: {},
390391
feeInfo: {},
391392
coin: coin.getChain(),
392393
};
394+
}
395+
396+
const rootWalletKeysWasm = fixedScriptWallet.RootWalletKeys.from(walletKeys);
397+
const replayProtection = { publicKeys: getReplayProtectionPubkeys(coin.network) };
398+
399+
// Sign with user key first
400+
psbt = signAndVerifyPsbt(psbt, walletKeys.user, rootWalletKeysWasm, replayProtection);
401+
402+
if (isKrsRecovery) {
403+
// The KRS provider keyternal solely supports P2SH, P2WSH, and P2SH-P2WSH input script types.
404+
// It currently uses an outdated BitGoJS SDK, which relies on a legacy transaction builder for cosigning.
405+
// Unfortunately, upgrading the keyternal code presents challenges,
406+
// which hinders the integration of the latest BitGoJS SDK with PSBT signing support.
407+
txInfo.transactionHex =
408+
params.krsProvider === 'keyternal'
409+
? utxolib.bitgo.extractP2msOnlyHalfSignedTx(toPsbtToUtxolibPsbt(psbt, coin.name)).toBuffer().toString('hex')
410+
: encodeTransaction(psbt).toString('hex');
393411
} else {
394-
signAndVerifyPsbt(psbt, walletKeys.user, { isLastSignature: false });
395-
if (isKrsRecovery) {
396-
// The KRS provider keyternal solely supports P2SH, P2WSH, and P2SH-P2WSH input script types.
397-
// It currently uses an outdated BitGoJS SDK, which relies on a legacy transaction builder for cosigning.
398-
// Unfortunately, upgrading the keyternal code presents challenges,
399-
// which hinders the integration of the latest BitGoJS SDK with PSBT signing support.
400-
txInfo.transactionHex =
401-
params.krsProvider === 'keyternal'
402-
? utxolib.bitgo.extractP2msOnlyHalfSignedTx(psbt).toBuffer().toString('hex')
403-
: psbt.toHex();
412+
// Sign with backup key
413+
psbt = signAndVerifyPsbt(psbt, walletKeys.backup, rootWalletKeysWasm, replayProtection);
414+
// Finalize and extract transaction
415+
psbt.finalizeAllInputs();
416+
if (psbt instanceof utxolib.bitgo.UtxoPsbt) {
417+
txInfo.transactionHex = psbt.extractTransaction().toBuffer().toString('hex');
418+
} else if (psbt instanceof fixedScriptWallet.BitGoPsbt) {
419+
txInfo.transactionHex = Buffer.from(psbt.extractTransaction()).toString('hex');
404420
} else {
405-
const tx = signAndVerifyPsbt(psbt, walletKeys.backup, { isLastSignature: true });
406-
txInfo.transactionHex = tx.toBuffer().toString('hex');
421+
throw new Error('expected a UtxoPsbt or BitGoPsbt object');
407422
}
408423
}
409424

410425
if (isKrsRecovery) {
411426
txInfo.coin = coin.getChain();
412427
txInfo.backupKey = params.backupKey;
413-
const recoveryAmount = getRecoveryAmount(psbt, params.recoveryDestination);
428+
const recoveryAmount = getRecoveryAmount(psbt, walletKeys, params.recoveryDestination);
414429
txInfo.recoveryAmount = Number(recoveryAmount);
415430
txInfo.recoveryAmountString = recoveryAmount.toString();
416431
}

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

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import * as utxolib from '@bitgo/utxo-lib';
22
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
33
import { Dimensions } from '@bitgo/unspents';
4-
import { fixedScriptWallet, CoinName } from '@bitgo/wasm-utxo';
4+
import { CoinName, fixedScriptWallet } from '@bitgo/wasm-utxo';
55
import { BitGoBase, IWallet, Keychain, Triple, Wallet } from '@bitgo/sdk-core';
66
import { decrypt } from '@bitgo/sdk-api';
77

88
import { AbstractUtxoCoin, TransactionInfo } from '../abstractUtxoCoin';
9-
import { signAndVerifyPsbt } from '../transaction/fixedScript/signPsbt';
9+
import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction';
10+
import { getNetworkFromCoinName } from '../names';
11+
import { encodeTransaction } from '../transaction/decode';
12+
import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection';
13+
import { toTNumber } from '../tnumber';
1014

1115
import {
1216
PsbtBackend,
1317
createEmptyWasmPsbt,
1418
addWalletInputsToWasmPsbt,
1519
addOutputToWasmPsbt,
16-
wasmPsbtToUtxolibPsbt,
20+
getRecoveryAmount,
1721
} from './psbt';
1822

1923
const { unspentSum } = utxolib.bitgo;
@@ -343,12 +347,13 @@ async function getPrv(xprv?: string, passphrase?: string, wallet?: IWallet | Wal
343347
* @return unsigned PSBT
344348
*/
345349
function createSweepTransactionUtxolib<TNumber extends number | bigint = number>(
346-
network: utxolib.Network,
350+
coinName: CoinName,
347351
walletKeys: RootWalletKeys,
348352
unspents: WalletUnspent<TNumber>[],
349353
targetAddress: string,
350354
feeRateSatVB: number
351355
): utxolib.bitgo.UtxoPsbt {
356+
const network = getNetworkFromCoinName(coinName);
352357
const inputValue = unspentSum<bigint>(
353358
unspents.map((u) => ({ ...u, value: BigInt(u.value) })),
354359
'bigint'
@@ -392,21 +397,20 @@ function createSweepTransactionUtxolib<TNumber extends number | bigint = number>
392397
* @return unsigned PSBT
393398
*/
394399
function createSweepTransactionWasm<TNumber extends number | bigint = number>(
395-
network: utxolib.Network,
400+
coinName: CoinName,
396401
walletKeys: RootWalletKeys,
397402
unspents: WalletUnspent<TNumber>[],
398403
targetAddress: string,
399-
feeRateSatVB: number,
400-
coinName: CoinName
401-
): utxolib.bitgo.UtxoPsbt {
404+
feeRateSatVB: number
405+
): fixedScriptWallet.BitGoPsbt {
402406
const inputValue = unspentSum<bigint>(
403407
unspents.map((u) => ({ ...u, value: BigInt(u.value) })),
404408
'bigint'
405409
);
406410

407411
// Create PSBT with wasm-utxo and add wallet inputs using shared utilities
408412
const unspentsBigint = unspents.map((u) => ({ ...u, value: BigInt(u.value) }));
409-
const wasmPsbt = createEmptyWasmPsbt(network, walletKeys);
413+
const wasmPsbt = createEmptyWasmPsbt(coinName, walletKeys);
410414
addWalletInputsToWasmPsbt(wasmPsbt, unspentsBigint, walletKeys);
411415

412416
// Calculate dimensions using wasm-utxo Dimensions
@@ -416,10 +420,10 @@ function createSweepTransactionWasm<TNumber extends number | bigint = number>(
416420
const fee = BigInt(Math.round(vsize * feeRateSatVB));
417421

418422
// Add output to wasm PSBT
419-
addOutputToWasmPsbt(wasmPsbt, targetAddress, inputValue - fee, network);
423+
addOutputToWasmPsbt(wasmPsbt, targetAddress, inputValue - fee, coinName);
420424

421425
// Convert to utxolib PSBT for signing and return
422-
return wasmPsbtToUtxolibPsbt(wasmPsbt, network);
426+
return wasmPsbt;
423427
}
424428

425429
/**
@@ -434,21 +438,17 @@ function createSweepTransactionWasm<TNumber extends number | bigint = number>(
434438
* @return unsigned PSBT
435439
*/
436440
function createSweepTransaction<TNumber extends number | bigint = number>(
437-
network: utxolib.Network,
441+
coinName: CoinName,
438442
walletKeys: RootWalletKeys,
439443
unspents: WalletUnspent<TNumber>[],
440444
targetAddress: string,
441445
feeRateSatVB: number,
442-
backend: PsbtBackend = 'wasm-utxo',
443-
coinName?: CoinName
444-
): utxolib.bitgo.UtxoPsbt {
446+
backend: PsbtBackend = 'wasm-utxo'
447+
): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt {
445448
if (backend === 'wasm-utxo') {
446-
if (!coinName) {
447-
throw new Error('coinName is required for wasm-utxo backend');
448-
}
449-
return createSweepTransactionWasm(network, walletKeys, unspents, targetAddress, feeRateSatVB, coinName);
449+
return createSweepTransactionWasm(coinName, walletKeys, unspents, targetAddress, feeRateSatVB);
450450
} else {
451-
return createSweepTransactionUtxolib(network, walletKeys, unspents, targetAddress, feeRateSatVB);
451+
return createSweepTransactionUtxolib(coinName, walletKeys, unspents, targetAddress, feeRateSatVB);
452452
}
453453
}
454454

@@ -502,36 +502,39 @@ export async function recoverCrossChain<TNumber extends number | bigint = number
502502
// Create PSBT for both signed and unsigned recovery
503503
// Use wasm-utxo for testnet coins only, utxolib for mainnet
504504
const backend: PsbtBackend = utxolib.isTestnet(params.sourceCoin.network) ? 'wasm-utxo' : 'utxolib';
505-
const psbt = createSweepTransaction<TNumber>(
506-
params.sourceCoin.network,
505+
let psbt = createSweepTransaction<TNumber>(
506+
params.sourceCoin.getChain(),
507507
walletKeys,
508508
walletUnspents,
509509
params.recoveryAddress,
510510
feeRateSatVB,
511-
backend,
512-
params.sourceCoin.getChain() as CoinName
511+
backend
513512
);
514513

515514
// For unsigned recovery, return unsigned PSBT hex
516515
if (!prv) {
517516
return {
518-
txHex: psbt.toHex(),
517+
txHex: encodeTransaction(psbt).toString('hex'),
519518
walletId: params.walletId,
520519
address: params.recoveryAddress,
521520
coin: params.sourceCoin.getChain(),
522521
};
523522
}
524523

525524
// For signed recovery, sign the PSBT with user key and return half-signed PSBT
526-
signAndVerifyPsbt(psbt, prv, { isLastSignature: false });
527-
const recoveryAmount = utxolib.bitgo.toTNumber<TNumber>(psbt.txOutputs[0].value, params.sourceCoin.amountType);
525+
psbt = signAndVerifyPsbt(psbt, prv, fixedScriptWallet.RootWalletKeys.from(walletKeys), {
526+
publicKeys: getReplayProtectionPubkeys(params.sourceCoin.network),
527+
});
528528

529529
return {
530530
version: wallet instanceof Wallet ? 2 : 1,
531531
walletId: params.walletId,
532-
txHex: psbt.toHex(),
532+
txHex: encodeTransaction(psbt).toString('hex'),
533533
sourceCoin: params.sourceCoin.getChain(),
534534
recoveryCoin: params.recoveryCoin.getChain(),
535-
recoveryAmount,
535+
recoveryAmount: toTNumber(
536+
getRecoveryAmount(psbt, walletKeys, params.recoveryAddress),
537+
params.sourceCoin.amountType
538+
) as TNumber,
536539
};
537540
}

0 commit comments

Comments
 (0)