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
82 changes: 79 additions & 3 deletions modules/abstract-utxo/src/recovery/crossChainRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import { decrypt } from '@bitgo/sdk-api';
import { AbstractUtxoCoin, TransactionInfo } from '../abstractUtxoCoin';
import { signAndVerifyPsbt } from '../transaction/fixedScript/signPsbt';

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

const { unspentSum } = utxolib.bitgo;
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
type Unspent<TNumber extends number | bigint = number> = utxolib.bitgo.Unspent<TNumber>;
Expand Down Expand Up @@ -325,15 +333,15 @@ async function getPrv(xprv?: string, passphrase?: string, wallet?: IWallet | Wal
}

/**
* Create a sweep transaction for cross-chain recovery using PSBT
* Create a sweep transaction for cross-chain recovery using PSBT (utxolib implementation)
* @param network
* @param walletKeys
* @param unspents
* @param targetAddress
* @param feeRateSatVB
* @return unsigned PSBT
*/
function createSweepTransaction<TNumber extends number | bigint = number>(
function createSweepTransactionUtxolib<TNumber extends number | bigint = number>(
network: utxolib.Network,
walletKeys: RootWalletKeys,
unspents: WalletUnspent<TNumber>[],
Expand Down Expand Up @@ -372,6 +380,71 @@ function createSweepTransaction<TNumber extends number | bigint = number>(
return psbt;
}

/**
* Create a sweep transaction for cross-chain recovery using wasm-utxo
* @param network
* @param walletKeys
* @param unspents
* @param targetAddress
* @param feeRateSatVB
* @return unsigned PSBT
*/
function createSweepTransactionWasm<TNumber extends number | bigint = number>(
network: utxolib.Network,
walletKeys: RootWalletKeys,
unspents: WalletUnspent<TNumber>[],
targetAddress: string,
feeRateSatVB: number
): utxolib.bitgo.UtxoPsbt {
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);
addWalletInputsToWasmPsbt(wasmPsbt, unspentsBigint, walletKeys);

// Convert to utxolib PSBT temporarily for dimension calculation
const tempPsbt = wasmPsbtToUtxolibPsbt(wasmPsbt, network);
const vsize = Dimensions.fromPsbt(tempPsbt)
.plus(Dimensions.fromOutput({ script: utxolib.address.toOutputScript(targetAddress, network) }))
.getVSize();
const fee = BigInt(Math.round(vsize * feeRateSatVB));

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

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

/**
* Create a sweep transaction for cross-chain recovery using PSBT
* @param network
* @param walletKeys
* @param unspents
* @param targetAddress
* @param feeRateSatVB
* @param backend - Which backend to use for PSBT creation (default: 'wasm-utxo')
* @return unsigned PSBT
*/
function createSweepTransaction<TNumber extends number | bigint = number>(
network: utxolib.Network,
walletKeys: RootWalletKeys,
unspents: WalletUnspent<TNumber>[],
targetAddress: string,
feeRateSatVB: number,
backend: PsbtBackend = 'wasm-utxo'
): utxolib.bitgo.UtxoPsbt {
if (backend === 'wasm-utxo') {
return createSweepTransactionWasm(network, walletKeys, unspents, targetAddress, feeRateSatVB);
} else {
return createSweepTransactionUtxolib(network, walletKeys, unspents, targetAddress, feeRateSatVB);
}
}

type RecoverParams = {
/** Wallet ID (can be v1 wallet or v2 wallet) */
walletId: string;
Expand Down Expand Up @@ -420,12 +493,15 @@ export async function recoverCrossChain<TNumber extends number | bigint = number
const feeRateSatVB = await getFeeRateSatVB(params.sourceCoin);

// 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,
walletKeys,
walletUnspents,
params.recoveryAddress,
feeRateSatVB
feeRateSatVB,
backend
);

// For unsigned recovery, return unsigned PSBT hex
Expand Down
120 changes: 94 additions & 26 deletions modules/abstract-utxo/src/recovery/psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type PsbtBackend = 'wasm-utxo' | 'utxolib';
/**
* Check if a chain code is for a taproot script type
*/
function isTaprootChain(chain: ChainCode): boolean {
export function isTaprootChain(chain: ChainCode): boolean {
return (
(chainCodesP2tr as readonly number[]).includes(chain) || (chainCodesP2trMusig2 as readonly number[]).includes(chain)
);
Expand All @@ -28,7 +28,7 @@ function isTaprootChain(chain: ChainCode): boolean {
/**
* Convert utxolib Network to wasm-utxo network name
*/
function toNetworkName(network: utxolib.Network): utxolibCompat.UtxolibName {
export function toNetworkName(network: utxolib.Network): utxolibCompat.UtxolibName {
const networkName = utxolib.getNetworkName(network);
if (!networkName) {
throw new Error(`Invalid network`);
Expand Down Expand Up @@ -129,32 +129,53 @@ const ZCASH_DEFAULT_BLOCK_HEIGHTS: Record<string, number> = {
};

/**
* Create a backup key recovery PSBT using wasm-utxo
* Options for creating an empty wasm-utxo PSBT
*/
function createBackupKeyRecoveryPsbtWasm(
export interface CreateEmptyWasmPsbtOptions {
/** Block height for Zcash networks (required to determine consensus branch ID) */
blockHeight?: number;
}

/**
* Create an empty wasm-utxo BitGoPsbt for a given network.
* Handles Zcash networks specially by using ZcashBitGoPsbt.
*
* @param network - The network for the PSBT
* @param rootWalletKeys - The wallet keys
* @param options - Optional settings (e.g., blockHeight for Zcash)
* @returns A wasm-utxo BitGoPsbt instance
*/
export function createEmptyWasmPsbt(
network: utxolib.Network,
rootWalletKeys: RootWalletKeys,
unspents: WalletUnspent<bigint>[],
options: CreateBackupKeyRecoveryPsbtOptions
): utxolib.bitgo.UtxoPsbt {
const { feeRateSatVB, recoveryDestination, keyRecoveryServiceFee, keyRecoveryServiceFeeAddress } = options;

options?: CreateEmptyWasmPsbtOptions
): fixedScriptWallet.BitGoPsbt {
const networkName = toNetworkName(network);

// Create PSBT with wasm-utxo and add wallet inputs
// wasm-utxo's RootWalletKeys.from() accepts utxolib's RootWalletKeys format (IWalletKeys interface)
let wasmPsbt: fixedScriptWallet.BitGoPsbt;

if (isZcashNetwork(networkName)) {
// For Zcash, use ZcashBitGoPsbt which requires block height to determine consensus branch ID
const blockHeight = options.blockHeight ?? ZCASH_DEFAULT_BLOCK_HEIGHTS[networkName];
wasmPsbt = fixedScriptWallet.ZcashBitGoPsbt.createEmpty(networkName as 'zcash' | 'zcashTest', rootWalletKeys, {
const blockHeight = options?.blockHeight ?? ZCASH_DEFAULT_BLOCK_HEIGHTS[networkName];
return fixedScriptWallet.ZcashBitGoPsbt.createEmpty(networkName as 'zcash' | 'zcashTest', rootWalletKeys, {
blockHeight,
});
} else {
wasmPsbt = fixedScriptWallet.BitGoPsbt.createEmpty(networkName, rootWalletKeys);
}

return fixedScriptWallet.BitGoPsbt.createEmpty(networkName, rootWalletKeys);
}

/**
* Add wallet inputs from unspents to a wasm-utxo BitGoPsbt.
* Handles taproot inputs by setting the appropriate signPath.
*
* @param wasmPsbt - The wasm-utxo BitGoPsbt to add inputs to
* @param unspents - The wallet unspents to add as inputs
* @param rootWalletKeys - The wallet keys
*/
export function addWalletInputsToWasmPsbt(
wasmPsbt: fixedScriptWallet.BitGoPsbt,
unspents: WalletUnspent<bigint>[],
rootWalletKeys: RootWalletKeys
): void {
unspents.forEach((unspent) => {
const { txid, vout } = utxolib.bitgo.parseOutputId(unspent.id);
const signPath: fixedScriptWallet.SignPath | undefined = isTaprootChain(unspent.chain)
Expand All @@ -178,11 +199,59 @@ function createBackupKeyRecoveryPsbtWasm(
}
);
});
}

// Convert wasm-utxo PSBT to utxolib PSBT for dimension calculation and output addition
const psbt = utxolib.bitgo.createPsbtFromBuffer(Buffer.from(wasmPsbt.serialize()), network);
/**
* Add an output to a wasm-utxo BitGoPsbt.
*
* @param wasmPsbt - The wasm-utxo BitGoPsbt to add the output to
* @param address - The destination address
* @param value - The output value in satoshis
* @param network - The network (used to convert address to script)
* @returns The output index
*/
export function addOutputToWasmPsbt(
wasmPsbt: fixedScriptWallet.BitGoPsbt,
address: string,
value: bigint,
network: utxolib.Network
): number {
const script = utxolib.address.toOutputScript(address, network);
return wasmPsbt.addOutput({ script: new Uint8Array(script), value });
}

let dimensions = Dimensions.fromPsbt(psbt).plus(
/**
* Convert a wasm-utxo BitGoPsbt to a utxolib UtxoPsbt.
*
* @param wasmPsbt - The wasm-utxo BitGoPsbt to convert
* @param network - The network
* @returns A utxolib UtxoPsbt
*/
export function wasmPsbtToUtxolibPsbt(
wasmPsbt: fixedScriptWallet.BitGoPsbt,
network: utxolib.Network
): utxolib.bitgo.UtxoPsbt {
return utxolib.bitgo.createPsbtFromBuffer(Buffer.from(wasmPsbt.serialize()), network);
}

/**
* Create a backup key recovery PSBT using wasm-utxo
*/
function createBackupKeyRecoveryPsbtWasm(
network: utxolib.Network,
rootWalletKeys: RootWalletKeys,
unspents: WalletUnspent<bigint>[],
options: CreateBackupKeyRecoveryPsbtOptions
): utxolib.bitgo.UtxoPsbt {
const { feeRateSatVB, recoveryDestination, keyRecoveryServiceFee, keyRecoveryServiceFeeAddress } = options;

// Create PSBT with wasm-utxo and add wallet inputs using shared utilities
const wasmPsbt = createEmptyWasmPsbt(network, rootWalletKeys, { blockHeight: options.blockHeight });
addWalletInputsToWasmPsbt(wasmPsbt, unspents, rootWalletKeys);

// Convert to utxolib PSBT temporarily for dimension calculation
const tempPsbt = wasmPsbtToUtxolibPsbt(wasmPsbt, network);
let dimensions = Dimensions.fromPsbt(tempPsbt).plus(
Dimensions.fromOutput({ script: utxolib.address.toOutputScript(recoveryDestination, network) })
);

Expand All @@ -202,16 +271,15 @@ function createBackupKeyRecoveryPsbtWasm(
throw new InsufficientFundsError(totalInputAmount, approximateFee, keyRecoveryServiceFee, recoveryAmount);
}

psbt.addOutput({ script: utxolib.address.toOutputScript(recoveryDestination, network), value: recoveryAmount });
// Add outputs to wasm PSBT
addOutputToWasmPsbt(wasmPsbt, recoveryDestination, recoveryAmount, network);

if (keyRecoveryServiceFeeAddress) {
psbt.addOutput({
script: utxolib.address.toOutputScript(keyRecoveryServiceFeeAddress, network),
value: keyRecoveryServiceFee,
});
addOutputToWasmPsbt(wasmPsbt, keyRecoveryServiceFeeAddress, keyRecoveryServiceFee, network);
}

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

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": 2,
"walletId": "5abacebe28d72fbd07e0b8cbba0ff39e",
"txHex": "70736274ff01005302000000014b0158b39d55b07ff198dc19f2c5c36b74408204a83ab9160523f0c2111b78910000000000ffffffff0188c2f5050000000017a9149c4525e9e9fc92cdda2043d35ad699c343dbab0f87000000004f010488b21e0000000000000000004b256d3cf3524c8d7086e295a1923d6fa2f99b686699ed50084bb114495c982403a86864862a9e315221809501f2a4200cd9e057a70f9164d485d4cfbeb8e47c74048374ad864f010488b21e000000000000000000914cc440157319de14126a1a2e87ea86f3b983f923fb17693a157b721220d74c02e81e105716179975cc47afd117cae272519aafdd6bfff688e4280d384e13184f04e15f6f214f010488b21e000000000000000000da28679577f7faf0ed86164da220aa4a29c7edfb0de8bdabd97f19fd15e74bed03db2b42af97f60db6ec5a1500e246ef2107660c4fc02699ed69b82c2f3e9324ae0403a823910001012018ddf5050000000017a9141e57a925dd863a86af341037e700862bf66bf7b6872202037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e474830450221009c29a7f6f5473fd3b2b3177f0f61de0162b339342605ecadd8f04b39bca9139802207937a1a4f66037d1d6238c5c9b2def7895af1e222bba6fab135fd83edb92feab41010304410000000104695221037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e472102658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d3978702102641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b853ae220602641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b8148374ad8600000000000000000000000000000000220602658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d39787014e15f6f21000000000000000000000000000000002206037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e471403a82391000000000000000000000000000000000000",
"txHex": "70736274ff01005302000000014b0158b39d55b07ff198dc19f2c5c36b74408204a83ab9160523f0c2111b78910000000000feffffff0188c2f5050000000017a9149c4525e9e9fc92cdda2043d35ad699c343dbab0f87000000004f010488b21e0000000000000000004b256d3cf3524c8d7086e295a1923d6fa2f99b686699ed50084bb114495c982403a86864862a9e315221809501f2a4200cd9e057a70f9164d485d4cfbeb8e47c74048374ad864f010488b21e000000000000000000914cc440157319de14126a1a2e87ea86f3b983f923fb17693a157b721220d74c02e81e105716179975cc47afd117cae272519aafdd6bfff688e4280d384e13184f04e15f6f214f010488b21e000000000000000000da28679577f7faf0ed86164da220aa4a29c7edfb0de8bdabd97f19fd15e74bed03db2b42af97f60db6ec5a1500e246ef2107660c4fc02699ed69b82c2f3e9324ae0403a823910001012018ddf5050000000017a9141e57a925dd863a86af341037e700862bf66bf7b6872202037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e47483045022100a7d7b412b2da3064818a87f524038ab5a796b2fb24e72d2ec13d363271cdeb390220519d8330223adf28a3d25027ff572786f78dc941b9a82770793271192c555abe41010304410000000104695221037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e472102658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d3978702102641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b853ae220602641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b8148374ad8600000000000000000000000000000000220602658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d39787014e15f6f21000000000000000000000000000000002206037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e471403a82391000000000000000000000000000000000000",
"sourceCoin": "tbch",
"recoveryCoin": "tbsv",
"recoveryAmount": 99992200
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"txHex": "70736274ff01005302000000014b0158b39d55b07ff198dc19f2c5c36b74408204a83ab9160523f0c2111b78910000000000ffffffff0188c2f5050000000017a9149c4525e9e9fc92cdda2043d35ad699c343dbab0f87000000004f010488b21e0000000000000000004b256d3cf3524c8d7086e295a1923d6fa2f99b686699ed50084bb114495c982403a86864862a9e315221809501f2a4200cd9e057a70f9164d485d4cfbeb8e47c74048374ad864f010488b21e000000000000000000914cc440157319de14126a1a2e87ea86f3b983f923fb17693a157b721220d74c02e81e105716179975cc47afd117cae272519aafdd6bfff688e4280d384e13184f04e15f6f214f010488b21e000000000000000000da28679577f7faf0ed86164da220aa4a29c7edfb0de8bdabd97f19fd15e74bed03db2b42af97f60db6ec5a1500e246ef2107660c4fc02699ed69b82c2f3e9324ae0403a823910001012018ddf5050000000017a9141e57a925dd863a86af341037e700862bf66bf7b687010304410000000104695221037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e472102658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d3978702102641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b853ae220602641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b8148374ad8600000000000000000000000000000000220602658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d39787014e15f6f21000000000000000000000000000000002206037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e471403a82391000000000000000000000000000000000000",
"txHex": "70736274ff01005302000000014b0158b39d55b07ff198dc19f2c5c36b74408204a83ab9160523f0c2111b78910000000000feffffff0188c2f5050000000017a9149c4525e9e9fc92cdda2043d35ad699c343dbab0f87000000004f010488b21e0000000000000000004b256d3cf3524c8d7086e295a1923d6fa2f99b686699ed50084bb114495c982403a86864862a9e315221809501f2a4200cd9e057a70f9164d485d4cfbeb8e47c74048374ad864f010488b21e000000000000000000914cc440157319de14126a1a2e87ea86f3b983f923fb17693a157b721220d74c02e81e105716179975cc47afd117cae272519aafdd6bfff688e4280d384e13184f04e15f6f214f010488b21e000000000000000000da28679577f7faf0ed86164da220aa4a29c7edfb0de8bdabd97f19fd15e74bed03db2b42af97f60db6ec5a1500e246ef2107660c4fc02699ed69b82c2f3e9324ae0403a823910001012018ddf5050000000017a9141e57a925dd863a86af341037e700862bf66bf7b687010304410000000104695221037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e472102658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d3978702102641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b853ae220602641ee6557561c9038242cafa7f538070d7646a969bcf6169f9950abfcfefd6b8148374ad8600000000000000000000000000000000220602658831a87322b3583515ca8725841335505755ada53ee133c70a6b4b8d39787014e15f6f21000000000000000000000000000000002206037acffd52bb7c39a4ac3d4c01af33ce0367afec45347e332edca63a38d1fb2e471403a82391000000000000000000000000000000000000",
"walletId": "5abacebe28d72fbd07e0b8cbba0ff39e",
"address": "2N7VWEhmfT8CzGSW2bCVeKJ3GCwSD1nsL2V",
"coin": "tbch"
Expand Down
Loading
Loading