Skip to content

Commit 2e344af

Browse files
Merge pull request #8057 from BitGo/BTC-2891.wasm-utxo-full-backup-ccr
feat(abstract-utxo): migrate recovery flows to wasm-utxo backend
2 parents 7cd76fc + 92d3e52 commit 2e344af

118 files changed

Lines changed: 417 additions & 588 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/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
"@bitgo/sdk-api": "^1.73.4",
6565
"@bitgo/sdk-core": "^36.30.0",
6666
"@bitgo/secp256k1": "^1.10.0",
67-
"@bitgo/unspents": "^0.51.0",
6867
"@bitgo/utxo-core": "^1.32.0",
6968
"@bitgo/utxo-lib": "^11.20.0",
7069
"@bitgo/utxo-ord": "^1.25.0",

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

Lines changed: 56 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import _ from 'lodash';
2-
import * as utxolib from '@bitgo/utxo-lib';
32
import {
43
BitGoBase,
54
ErrorNoInputToRecover,
@@ -9,32 +8,29 @@ import {
98
getIsUnsignedSweep,
109
isTriple,
1110
krsProviders,
11+
Triple,
1212
} from '@bitgo/sdk-core';
13-
import { getMainnet, networks } from '@bitgo/utxo-lib';
14-
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
13+
import { BIP32, fixedScriptWallet } from '@bitgo/wasm-utxo';
1514

1615
import { AbstractUtxoCoin } from '../abstractUtxoCoin';
1716
import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction';
1817
import { generateAddressWithChainAndIndex } from '../address';
1918
import { encodeTransaction } from '../transaction/decode';
2019
import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection';
21-
import { isTestnetCoin, UtxoCoinName } from '../names';
22-
import type { WalletUnspent } from '../unspent';
20+
import { getMainnetCoinName, UtxoCoinName } from '../names';
21+
import { parseOutputId, unspentSum, type WalletUnspent } from '../unspent';
2322

2423
import { forCoin, RecoveryProvider } from './RecoveryProvider';
2524
import { MempoolApi } from './mempoolApi';
2625
import { CoingeckoApi } from './coingeckoApi';
27-
import { createBackupKeyRecoveryPsbt, getRecoveryAmount, PsbtBackend, toPsbtToUtxolibPsbt } from './psbt';
26+
import { createBackupKeyRecoveryPsbt, getRecoveryAmount } from './psbt';
2827

29-
type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
30-
type ChainCode = utxolib.bitgo.ChainCode;
31-
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
28+
type ScriptType2Of3 = fixedScriptWallet.OutputScriptType;
29+
type ChainCode = fixedScriptWallet.ChainCode;
3230
type WalletUnspentJSON = WalletUnspent & {
3331
valueString: string;
3432
};
3533

36-
const { getInternalChainCode, scriptTypeForChain, outputScripts, getExternalChainCode } = utxolib.bitgo;
37-
3834
// V1 only deals with BTC. 50 sat/vbyte is very arbitrary.
3935
export const DEFAULT_RECOVERY_FEERATE_SAT_VBYTE_V1 = 50;
4036

@@ -120,7 +116,7 @@ export interface RecoverParams {
120116
function getFormattedAddress(
121117
coin: AbstractUtxoCoin,
122118
coinName: UtxoCoinName,
123-
walletKeys: RootWalletKeys,
119+
walletKeys: fixedScriptWallet.RootWalletKeys,
124120
chain: ChainCode,
125121
addrIndex: number
126122
): string {
@@ -131,15 +127,18 @@ function getFormattedAddress(
131127
return format === 'cashaddr' ? address.split(':')[1] : address;
132128
}
133129

130+
function hasWitnessData(scriptType: ScriptType2Of3): boolean {
131+
return scriptType !== 'p2sh';
132+
}
133+
134134
async function queryBlockchainUnspentsPath(
135135
coin: AbstractUtxoCoin,
136136
params: RecoverParams,
137-
walletKeys: RootWalletKeys,
137+
walletKeys: fixedScriptWallet.RootWalletKeys,
138138
chain: ChainCode
139139
): Promise<WalletUnspent<bigint>[]> {
140-
const scriptType = scriptTypeForChain(chain);
141-
const fetchPrevTx =
142-
!utxolib.bitgo.outputScripts.hasWitnessData(scriptType) && getMainnet(coin.network) !== networks.zcash;
140+
const scriptType = fixedScriptWallet.ChainCode.scriptType(chain);
141+
const fetchPrevTx = !hasWitnessData(scriptType) && getMainnetCoinName(coin.name) !== 'zec';
143142
const recoveryProvider = params.recoveryProvider ?? forCoin(coin.getChain(), params.apiKey);
144143
const MAX_SEQUENTIAL_ADDRESSES_WITHOUT_TXS = params.scan || 20;
145144
let numSequentialAddressesWithoutTxs = 0;
@@ -168,7 +167,7 @@ async function queryBlockchainUnspentsPath(
168167
const addressUnspents = await recoveryProvider.getUnspentsForAddresses([formattedAddress]);
169168
const processedUnspents = await Promise.all(
170169
addressUnspents.map(async (u): Promise<WalletUnspent<bigint>> => {
171-
const { txid, vout } = utxolib.bitgo.parseOutputId(u.id);
170+
const { txid, vout } = parseOutputId(u.id);
172171
let val = BigInt(u.value);
173172
if (coin.amountType === 'bigint') {
174173
// blockchair returns the number with the correct precision, but in number format
@@ -246,6 +245,14 @@ export type BackupKeyRecoveryTransansaction = {
246245
recoveryAmountString: string;
247246
};
248247

248+
function getBip32Privkeys(bitgo: BitGoBase, params: RecoverParams): Triple<BIP32> {
249+
const keys = getBip32Keys(bitgo, params, { requireBitGoXpub: true });
250+
if (!isTriple(keys)) {
251+
throw new Error(`expected key triple`);
252+
}
253+
return keys.map((k) => BIP32.from(k.toBase58())) as Triple<BIP32>;
254+
}
255+
249256
/**
250257
* Builds a funds recovery transaction without BitGo.
251258
*
@@ -303,35 +310,43 @@ export async function backupKeyRecovery(
303310
const krsProvider = isKrsRecovery ? getKrsProvider(coin, params.krsProvider) : undefined;
304311

305312
// check whether key material and password authenticate the users and return parent keys of all three keys of the wallet
306-
const keys = getBip32Keys(bitgo, params, { requireBitGoXpub: true });
307-
if (!isTriple(keys)) {
308-
throw new Error(`expected key triple`);
309-
}
310-
const walletKeys = new utxolib.bitgo.RootWalletKeys(keys, [
311-
params.userKeyPath || utxolib.bitgo.RootWalletKeys.defaultPrefix,
312-
utxolib.bitgo.RootWalletKeys.defaultPrefix,
313-
utxolib.bitgo.RootWalletKeys.defaultPrefix,
314-
]);
313+
const keys = getBip32Privkeys(bitgo, params);
314+
const walletKeys = fixedScriptWallet.RootWalletKeys.from({
315+
triple: keys,
316+
derivationPrefixes: [params.userKeyPath || 'm/0/0', 'm/0/0', 'm/0/0'],
317+
});
315318

316319
const unspents: WalletUnspent<bigint>[] = (
317320
await Promise.all(
318-
outputScripts.scriptTypes2Of3
321+
fixedScriptWallet.outputScriptTypes
319322
.filter(
320-
(addressType) => coin.supportsAddressType(addressType) && !params.ignoreAddressTypes?.includes(addressType)
323+
(addressType) =>
324+
fixedScriptWallet.supportsScriptType(coin.name, addressType) &&
325+
!params.ignoreAddressTypes?.includes(addressType)
321326
)
322327
.reduce(
323328
(queries, addressType) => [
324329
...queries,
325-
queryBlockchainUnspentsPath(coin, params, walletKeys, getExternalChainCode(addressType)),
326-
queryBlockchainUnspentsPath(coin, params, walletKeys, getInternalChainCode(addressType)),
330+
queryBlockchainUnspentsPath(
331+
coin,
332+
params,
333+
walletKeys,
334+
fixedScriptWallet.ChainCode.value(addressType, 'external')
335+
),
336+
queryBlockchainUnspentsPath(
337+
coin,
338+
params,
339+
walletKeys,
340+
fixedScriptWallet.ChainCode.value(addressType, 'internal')
341+
),
327342
],
328343
[] as Promise<WalletUnspent<bigint>[]>[]
329344
)
330345
)
331346
).flat();
332347

333348
// Execute the queries and gather the unspents
334-
const totalInputAmount = utxolib.bitgo.unspentSum(unspents, 'bigint');
349+
const totalInputAmount = unspentSum(unspents);
335350
if (totalInputAmount <= BigInt(0)) {
336351
throw new ErrorNoInputToRecover();
337352
}
@@ -370,20 +385,12 @@ export async function backupKeyRecovery(
370385
}
371386
}
372387

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-
);
388+
let psbt = createBackupKeyRecoveryPsbt(coin.getChain(), walletKeys, unspents, {
389+
feeRateSatVB: feePerByte,
390+
recoveryDestination: params.recoveryDestination,
391+
keyRecoveryServiceFee: krsFee,
392+
keyRecoveryServiceFeeAddress: krsFeeAddress,
393+
});
387394

388395
if (isUnsignedSweep) {
389396
return {
@@ -398,7 +405,7 @@ export async function backupKeyRecovery(
398405
const replayProtection = { publicKeys: getReplayProtectionPubkeys(coin.name) };
399406

400407
// Sign with user key first
401-
psbt = signAndVerifyPsbt(psbt, walletKeys.user, rootWalletKeysWasm, replayProtection);
408+
psbt = signAndVerifyPsbt(psbt, keys[0], rootWalletKeysWasm, replayProtection);
402409

403410
if (isKrsRecovery) {
404411
// The KRS provider keyternal solely supports P2SH, P2WSH, and P2SH-P2WSH input script types.
@@ -407,20 +414,14 @@ export async function backupKeyRecovery(
407414
// which hinders the integration of the latest BitGoJS SDK with PSBT signing support.
408415
txInfo.transactionHex =
409416
params.krsProvider === 'keyternal'
410-
? utxolib.bitgo.extractP2msOnlyHalfSignedTx(toPsbtToUtxolibPsbt(psbt, coin.name)).toBuffer().toString('hex')
417+
? Buffer.from(psbt.getHalfSignedLegacyFormat()).toString('hex')
411418
: encodeTransaction(psbt).toString('hex');
412419
} else {
413420
// Sign with backup key
414-
psbt = signAndVerifyPsbt(psbt, walletKeys.backup, rootWalletKeysWasm, replayProtection);
421+
psbt = signAndVerifyPsbt(psbt, keys[1], rootWalletKeysWasm, replayProtection);
415422
// Finalize and extract transaction
416423
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-
}
424+
txInfo.transactionHex = Buffer.from(psbt.extractTransaction().toBytes()).toString('hex');
424425
}
425426

426427
if (isKrsRecovery) {

0 commit comments

Comments
 (0)