Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 35 additions & 20 deletions modules/abstract-utxo/src/recovery/backupKeyRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ import {
krsProviders,
} from '@bitgo/sdk-core';
import { getMainnet, networks } from '@bitgo/utxo-lib';
import { CoinName } from '@bitgo/wasm-utxo';
import { fixedScriptWallet } from '@bitgo/wasm-utxo';

import { AbstractUtxoCoin } from '../abstractUtxoCoin';
import { signAndVerifyPsbt } from '../transaction/fixedScript/signPsbt';
import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction';
import { generateAddressWithChainAndIndex } from '../address';
import { encodeTransaction } from '../transaction/decode';
import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection';

import { forCoin, RecoveryProvider } from './RecoveryProvider';
import { MempoolApi } from './mempoolApi';
import { CoingeckoApi } from './coingeckoApi';
import { createBackupKeyRecoveryPsbt, getRecoveryAmount, PsbtBackend } from './psbt';
import { createBackupKeyRecoveryPsbt, getRecoveryAmount, PsbtBackend, toPsbtToUtxolibPsbt } from './psbt';

type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
type ChainCode = utxolib.bitgo.ChainCode;
Expand Down Expand Up @@ -369,48 +371,61 @@ export async function backupKeyRecovery(

// Use wasm-utxo for testnet coins only, utxolib for mainnet
const backend: PsbtBackend = utxolib.isTestnet(coin.network) ? 'wasm-utxo' : 'utxolib';
const psbt = createBackupKeyRecoveryPsbt(
coin.network,
let psbt = createBackupKeyRecoveryPsbt(
coin.getChain(),
walletKeys,
unspents,
{
feeRateSatVB: feePerByte,
recoveryDestination: params.recoveryDestination,
keyRecoveryServiceFee: krsFee,
keyRecoveryServiceFeeAddress: krsFeeAddress,
coinName: coin.getChain() as CoinName,
},
backend
);

if (isUnsignedSweep) {
return {
txHex: psbt.toHex(),
txHex: encodeTransaction(psbt).toString('hex'),
txInfo: {},
feeInfo: {},
coin: coin.getChain(),
};
}

const rootWalletKeysWasm = fixedScriptWallet.RootWalletKeys.from(walletKeys);
const replayProtection = { publicKeys: getReplayProtectionPubkeys(coin.network) };

// Sign with user key first
psbt = signAndVerifyPsbt(psbt, walletKeys.user, rootWalletKeysWasm, replayProtection);

if (isKrsRecovery) {
// The KRS provider keyternal solely supports P2SH, P2WSH, and P2SH-P2WSH input script types.
// It currently uses an outdated BitGoJS SDK, which relies on a legacy transaction builder for cosigning.
// Unfortunately, upgrading the keyternal code presents challenges,
// which hinders the integration of the latest BitGoJS SDK with PSBT signing support.
txInfo.transactionHex =
params.krsProvider === 'keyternal'
? utxolib.bitgo.extractP2msOnlyHalfSignedTx(toPsbtToUtxolibPsbt(psbt, coin.name)).toBuffer().toString('hex')
: encodeTransaction(psbt).toString('hex');
} else {
signAndVerifyPsbt(psbt, walletKeys.user, { isLastSignature: false });
if (isKrsRecovery) {
// The KRS provider keyternal solely supports P2SH, P2WSH, and P2SH-P2WSH input script types.
// It currently uses an outdated BitGoJS SDK, which relies on a legacy transaction builder for cosigning.
// Unfortunately, upgrading the keyternal code presents challenges,
// which hinders the integration of the latest BitGoJS SDK with PSBT signing support.
txInfo.transactionHex =
params.krsProvider === 'keyternal'
? utxolib.bitgo.extractP2msOnlyHalfSignedTx(psbt).toBuffer().toString('hex')
: psbt.toHex();
// Sign with backup key
psbt = signAndVerifyPsbt(psbt, walletKeys.backup, rootWalletKeysWasm, replayProtection);
// Finalize and extract transaction
psbt.finalizeAllInputs();
if (psbt instanceof utxolib.bitgo.UtxoPsbt) {
txInfo.transactionHex = psbt.extractTransaction().toBuffer().toString('hex');
} else if (psbt instanceof fixedScriptWallet.BitGoPsbt) {
txInfo.transactionHex = Buffer.from(psbt.extractTransaction()).toString('hex');
} else {
const tx = signAndVerifyPsbt(psbt, walletKeys.backup, { isLastSignature: true });
txInfo.transactionHex = tx.toBuffer().toString('hex');
throw new Error('expected a UtxoPsbt or BitGoPsbt object');
}
}

if (isKrsRecovery) {
txInfo.coin = coin.getChain();
txInfo.backupKey = params.backupKey;
const recoveryAmount = getRecoveryAmount(psbt, params.recoveryDestination);
const recoveryAmount = getRecoveryAmount(psbt, walletKeys, params.recoveryDestination);
txInfo.recoveryAmount = Number(recoveryAmount);
txInfo.recoveryAmountString = recoveryAmount.toString();
}
Expand Down
61 changes: 32 additions & 29 deletions modules/abstract-utxo/src/recovery/crossChainRecovery.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import * as utxolib from '@bitgo/utxo-lib';
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
import { Dimensions } from '@bitgo/unspents';
import { fixedScriptWallet, CoinName } from '@bitgo/wasm-utxo';
import { CoinName, fixedScriptWallet } from '@bitgo/wasm-utxo';
import { BitGoBase, IWallet, Keychain, Triple, Wallet } from '@bitgo/sdk-core';
import { decrypt } from '@bitgo/sdk-api';

import { AbstractUtxoCoin, TransactionInfo } from '../abstractUtxoCoin';
import { signAndVerifyPsbt } from '../transaction/fixedScript/signPsbt';
import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction';
import { getNetworkFromCoinName } from '../names';
import { encodeTransaction } from '../transaction/decode';
import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection';
import { toTNumber } from '../tnumber';

import {
PsbtBackend,
createEmptyWasmPsbt,
addWalletInputsToWasmPsbt,
addOutputToWasmPsbt,
wasmPsbtToUtxolibPsbt,
getRecoveryAmount,
} from './psbt';

const { unspentSum } = utxolib.bitgo;
Expand Down Expand Up @@ -343,12 +347,13 @@ async function getPrv(xprv?: string, passphrase?: string, wallet?: IWallet | Wal
* @return unsigned PSBT
*/
function createSweepTransactionUtxolib<TNumber extends number | bigint = number>(
network: utxolib.Network,
coinName: CoinName,
walletKeys: RootWalletKeys,
unspents: WalletUnspent<TNumber>[],
targetAddress: string,
feeRateSatVB: number
): utxolib.bitgo.UtxoPsbt {
const network = getNetworkFromCoinName(coinName);
const inputValue = unspentSum<bigint>(
unspents.map((u) => ({ ...u, value: BigInt(u.value) })),
'bigint'
Expand Down Expand Up @@ -392,21 +397,20 @@ function createSweepTransactionUtxolib<TNumber extends number | bigint = number>
* @return unsigned PSBT
*/
function createSweepTransactionWasm<TNumber extends number | bigint = number>(
network: utxolib.Network,
coinName: CoinName,
walletKeys: RootWalletKeys,
unspents: WalletUnspent<TNumber>[],
targetAddress: string,
feeRateSatVB: number,
coinName: CoinName
): utxolib.bitgo.UtxoPsbt {
feeRateSatVB: number
): fixedScriptWallet.BitGoPsbt {
const inputValue = unspentSum<bigint>(
unspents.map((u) => ({ ...u, value: BigInt(u.value) })),
'bigint'
);

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

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

// Add output to wasm PSBT
addOutputToWasmPsbt(wasmPsbt, targetAddress, inputValue - fee, network);
addOutputToWasmPsbt(wasmPsbt, targetAddress, inputValue - fee, coinName);

// Convert to utxolib PSBT for signing and return
return wasmPsbtToUtxolibPsbt(wasmPsbt, network);
return wasmPsbt;
}

/**
Expand All @@ -434,21 +438,17 @@ function createSweepTransactionWasm<TNumber extends number | bigint = number>(
* @return unsigned PSBT
*/
function createSweepTransaction<TNumber extends number | bigint = number>(
network: utxolib.Network,
coinName: CoinName,
walletKeys: RootWalletKeys,
unspents: WalletUnspent<TNumber>[],
targetAddress: string,
feeRateSatVB: number,
backend: PsbtBackend = 'wasm-utxo',
coinName?: CoinName
): utxolib.bitgo.UtxoPsbt {
backend: PsbtBackend = 'wasm-utxo'
): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt {
if (backend === 'wasm-utxo') {
if (!coinName) {
throw new Error('coinName is required for wasm-utxo backend');
}
return createSweepTransactionWasm(network, walletKeys, unspents, targetAddress, feeRateSatVB, coinName);
return createSweepTransactionWasm(coinName, walletKeys, unspents, targetAddress, feeRateSatVB);
} else {
return createSweepTransactionUtxolib(network, walletKeys, unspents, targetAddress, feeRateSatVB);
return createSweepTransactionUtxolib(coinName, walletKeys, unspents, targetAddress, feeRateSatVB);
}
}

Expand Down Expand Up @@ -502,36 +502,39 @@ export async function recoverCrossChain<TNumber extends number | bigint = number
// Create PSBT for both signed and unsigned recovery
// Use wasm-utxo for testnet coins only, utxolib for mainnet
const backend: PsbtBackend = utxolib.isTestnet(params.sourceCoin.network) ? 'wasm-utxo' : 'utxolib';
const psbt = createSweepTransaction<TNumber>(
params.sourceCoin.network,
let psbt = createSweepTransaction<TNumber>(
params.sourceCoin.getChain(),
walletKeys,
walletUnspents,
params.recoveryAddress,
feeRateSatVB,
backend,
params.sourceCoin.getChain() as CoinName
backend
);

// For unsigned recovery, return unsigned PSBT hex
if (!prv) {
return {
txHex: psbt.toHex(),
txHex: encodeTransaction(psbt).toString('hex'),
walletId: params.walletId,
address: params.recoveryAddress,
coin: params.sourceCoin.getChain(),
};
}

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

return {
version: wallet instanceof Wallet ? 2 : 1,
walletId: params.walletId,
txHex: psbt.toHex(),
txHex: encodeTransaction(psbt).toString('hex'),
sourceCoin: params.sourceCoin.getChain(),
recoveryCoin: params.recoveryCoin.getChain(),
recoveryAmount,
recoveryAmount: toTNumber(
getRecoveryAmount(psbt, walletKeys, params.recoveryAddress),
params.sourceCoin.amountType
) as TNumber,
};
}
Loading