diff --git a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts index e7f0436006..0e0e2dde7a 100644 --- a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts +++ b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts @@ -100,6 +100,16 @@ export interface RecoverParams { recoveryProvider?: RecoveryProvider; /** Satoshi per byte */ feeRate?: number; + /** + * Transaction lock time (nLockTime). Set before signing — it is part of the sighash. + * Used for ECX replay-protection sweeps (e.g. 499_999_999) and other custom lock-time needs. + */ + lockTime?: number; + /** + * Input sequence number applied to every input. Defaults to 0xFFFFFFFE (RBF, non-final) + * which already satisfies the non-final requirement for lockTime to take effect. + */ + sequence?: number; } /** @@ -260,6 +270,10 @@ export interface RecoverWithUnspentsParams { krsFee?: bigint; /** KRS fee address (required if krsFee > 0) */ krsFeeAddress?: string; + /** Transaction lock time (nLockTime), set on the PSBT before signing */ + lockTime?: number; + /** Input sequence number applied to every input */ + sequence?: number; } function hasPrivateKey(key: BIP32): boolean { @@ -299,6 +313,8 @@ export function backupKeyRecoveryWithWalletUnspents( recoveryDestination: recoveryDestination, keyRecoveryServiceFee: krsFee ?? BigInt(0), keyRecoveryServiceFeeAddress: krsFeeAddress, + lockTime: params.lockTime, + sequence: params.sequence, }); const userHasPrivateKey = hasPrivateKey(keys[0]); @@ -549,6 +565,8 @@ export async function backupKeyRecovery( feeRateSatVB, krsFee, krsFeeAddress, + lockTime: params.lockTime, + sequence: params.sequence, }, unspents ); diff --git a/modules/abstract-utxo/src/recovery/psbt.ts b/modules/abstract-utxo/src/recovery/psbt.ts index 01b7247976..bb28f75102 100644 --- a/modules/abstract-utxo/src/recovery/psbt.ts +++ b/modules/abstract-utxo/src/recovery/psbt.ts @@ -34,6 +34,10 @@ interface CreateBackupKeyRecoveryPsbtOptions { keyRecoveryServiceFeeAddress: string | undefined; /** Block height for Zcash networks (required to determine consensus branch ID) */ blockHeight?: number; + /** Transaction lock time (nLockTime), set on the PSBT before signing */ + lockTime?: number; + /** Input sequence number applied to every input */ + sequence?: number; } /** @@ -59,6 +63,8 @@ const ZCASH_DEFAULT_BLOCK_HEIGHTS: Record<'zec' | 'tzec', number> = { export interface CreateEmptyWasmPsbtOptions { /** Block height for Zcash networks (required to determine consensus branch ID) */ blockHeight?: number; + /** Transaction lock time (nLockTime) */ + lockTime?: number; } /** @@ -80,10 +86,13 @@ export function createEmptyWasmPsbt( const blockHeight = options?.blockHeight ?? ZCASH_DEFAULT_BLOCK_HEIGHTS[coinName]; return fixedScriptWallet.ZcashBitGoPsbt.createEmpty(coinName, rootWalletKeys, { blockHeight, + lockTime: options?.lockTime, }); } - return fixedScriptWallet.BitGoPsbt.createEmpty(coinName, rootWalletKeys); + return fixedScriptWallet.BitGoPsbt.createEmpty(coinName, rootWalletKeys, { + lockTime: options?.lockTime, + }); } /** @@ -97,7 +106,8 @@ export function createEmptyWasmPsbt( export function addWalletInputsToWasmPsbt( wasmPsbt: fixedScriptWallet.BitGoPsbt, unspents: WalletUnspent[], - rootWalletKeys: fixedScriptWallet.RootWalletKeys + rootWalletKeys: fixedScriptWallet.RootWalletKeys, + sequence?: number ): void { unspents.forEach((unspent) => { const { txid, vout } = parseOutputId(unspent.id); @@ -113,6 +123,7 @@ export function addWalletInputsToWasmPsbt( txid, vout, value: unspent.value, + sequence, prevTx: prevTx, }, rootWalletKeys, @@ -155,8 +166,11 @@ function createBackupKeyRecoveryPsbtWasm( const { feeRateSatVB, recoveryDestination, keyRecoveryServiceFee, keyRecoveryServiceFeeAddress } = options; // Create PSBT with wasm-utxo and add wallet inputs using shared utilities - const wasmPsbt = createEmptyWasmPsbt(coinName, rootWalletKeys, { blockHeight: options.blockHeight }); - addWalletInputsToWasmPsbt(wasmPsbt, unspents, rootWalletKeys); + const wasmPsbt = createEmptyWasmPsbt(coinName, rootWalletKeys, { + blockHeight: options.blockHeight, + lockTime: options.lockTime, + }); + addWalletInputsToWasmPsbt(wasmPsbt, unspents, rootWalletKeys, options.sequence); // Calculate dimensions using wasm-utxo Dimensions let dimensions = fixedScriptWallet.Dimensions.fromPsbt(wasmPsbt).plus(