11import * as utxolib from '@bitgo/utxo-lib' ;
22import { Dimensions } from '@bitgo/unspents' ;
3+ import { fixedScriptWallet , utxolibCompat } from '@bitgo/wasm-utxo' ;
34
45type RootWalletKeys = utxolib . bitgo . RootWalletKeys ;
56type WalletUnspent < TNumber extends number | bigint > = utxolib . bitgo . WalletUnspent < TNumber > ;
67
8+ const { chainCodesP2tr, chainCodesP2trMusig2 } = utxolib . bitgo ;
9+
10+ type ChainCode = utxolib . bitgo . ChainCode ;
11+
12+ /**
13+ * Backend to use for PSBT creation.
14+ * - 'wasm-utxo': Use wasm-utxo for PSBT creation (default)
15+ * - 'utxolib': Use utxolib for PSBT creation (legacy)
16+ */
17+ export type PsbtBackend = 'wasm-utxo' | 'utxolib' ;
18+
19+ /**
20+ * Check if a chain code is for a taproot script type
21+ */
22+ function isTaprootChain ( chain : ChainCode ) : boolean {
23+ return (
24+ ( chainCodesP2tr as readonly number [ ] ) . includes ( chain ) || ( chainCodesP2trMusig2 as readonly number [ ] ) . includes ( chain )
25+ ) ;
26+ }
27+
28+ /**
29+ * Convert utxolib Network to wasm-utxo network name
30+ */
31+ function toNetworkName ( network : utxolib . Network ) : utxolibCompat . UtxolibName {
32+ const networkName = utxolib . getNetworkName ( network ) ;
33+ if ( ! networkName ) {
34+ throw new Error ( `Invalid network` ) ;
35+ }
36+ return networkName ;
37+ }
38+
739class InsufficientFundsError extends Error {
840 constructor (
941 public totalInputAmount : bigint ,
@@ -21,25 +53,25 @@ class InsufficientFundsError extends Error {
2153 }
2254}
2355
24- export function createBackupKeyRecoveryPsbt (
56+ interface CreateBackupKeyRecoveryPsbtOptions {
57+ feeRateSatVB : number ;
58+ recoveryDestination : string ;
59+ keyRecoveryServiceFee : bigint ;
60+ keyRecoveryServiceFeeAddress : string | undefined ;
61+ /** Block height for Zcash networks (required to determine consensus branch ID) */
62+ blockHeight ?: number ;
63+ }
64+
65+ /**
66+ * Create a backup key recovery PSBT using utxolib (legacy implementation)
67+ */
68+ function createBackupKeyRecoveryPsbtUtxolib (
2569 network : utxolib . Network ,
2670 rootWalletKeys : RootWalletKeys ,
2771 unspents : WalletUnspent < bigint > [ ] ,
28- {
29- feeRateSatVB,
30- recoveryDestination,
31- keyRecoveryServiceFee,
32- keyRecoveryServiceFeeAddress,
33- } : {
34- feeRateSatVB : number ;
35- recoveryDestination : string ;
36- keyRecoveryServiceFee : bigint ;
37- keyRecoveryServiceFeeAddress : string | undefined ;
38- }
72+ options : CreateBackupKeyRecoveryPsbtOptions
3973) : utxolib . bitgo . UtxoPsbt {
40- if ( keyRecoveryServiceFee > 0 && ! keyRecoveryServiceFeeAddress ) {
41- throw new Error ( 'keyRecoveryServiceFeeAddress is required when keyRecoveryServiceFee is provided' ) ;
42- }
74+ const { feeRateSatVB, recoveryDestination, keyRecoveryServiceFee, keyRecoveryServiceFeeAddress } = options ;
4375
4476 const psbt = utxolib . bitgo . createPsbtForNetwork ( { network } ) ;
4577 utxolib . bitgo . addXpubsToPsbt ( psbt , rootWalletKeys ) ;
@@ -60,12 +92,112 @@ export function createBackupKeyRecoveryPsbt(
6092 }
6193
6294 const approximateFee = BigInt ( dimensions . getVSize ( ) * feeRateSatVB ) ;
63-
6495 const totalInputAmount = utxolib . bitgo . unspentSum ( unspents , 'bigint' ) ;
96+ const recoveryAmount = totalInputAmount - approximateFee - keyRecoveryServiceFee ;
97+
98+ if ( recoveryAmount < BigInt ( 0 ) ) {
99+ throw new InsufficientFundsError ( totalInputAmount , approximateFee , keyRecoveryServiceFee , recoveryAmount ) ;
100+ }
101+
102+ psbt . addOutput ( { script : utxolib . address . toOutputScript ( recoveryDestination , network ) , value : recoveryAmount } ) ;
103+
104+ if ( keyRecoveryServiceFeeAddress ) {
105+ psbt . addOutput ( {
106+ script : utxolib . address . toOutputScript ( keyRecoveryServiceFeeAddress , network ) ,
107+ value : keyRecoveryServiceFee ,
108+ } ) ;
109+ }
110+
111+ return psbt ;
112+ }
113+
114+ /**
115+ * Check if the network is a Zcash network
116+ */
117+ function isZcashNetwork ( networkName : utxolibCompat . UtxolibName ) : boolean {
118+ return networkName === 'zcash' || networkName === 'zcashTest' ;
119+ }
120+
121+ /**
122+ * Default block heights for Zcash networks if not provided.
123+ * These should be set to a height after the latest network upgrade.
124+ * TODO(BTC-2901): get the height from blockchair API instead of hardcoding.
125+ */
126+ const ZCASH_DEFAULT_BLOCK_HEIGHTS : Record < string , number > = {
127+ zcash : 3146400 ,
128+ zcashTest : 3536500 ,
129+ } ;
130+
131+ /**
132+ * Create a backup key recovery PSBT using wasm-utxo
133+ */
134+ function createBackupKeyRecoveryPsbtWasm (
135+ network : utxolib . Network ,
136+ rootWalletKeys : RootWalletKeys ,
137+ unspents : WalletUnspent < bigint > [ ] ,
138+ options : CreateBackupKeyRecoveryPsbtOptions
139+ ) : utxolib . bitgo . UtxoPsbt {
140+ const { feeRateSatVB, recoveryDestination, keyRecoveryServiceFee, keyRecoveryServiceFeeAddress } = options ;
141+
142+ const networkName = toNetworkName ( network ) ;
143+
144+ // Create PSBT with wasm-utxo and add wallet inputs
145+ // wasm-utxo's RootWalletKeys.from() accepts utxolib's RootWalletKeys format (IWalletKeys interface)
146+ let wasmPsbt : fixedScriptWallet . BitGoPsbt ;
147+
148+ if ( isZcashNetwork ( networkName ) ) {
149+ // For Zcash, use ZcashBitGoPsbt which requires block height to determine consensus branch ID
150+ const blockHeight = options . blockHeight ?? ZCASH_DEFAULT_BLOCK_HEIGHTS [ networkName ] ;
151+ wasmPsbt = fixedScriptWallet . ZcashBitGoPsbt . createEmpty ( networkName as 'zcash' | 'zcashTest' , rootWalletKeys , {
152+ blockHeight,
153+ } ) ;
154+ } else {
155+ wasmPsbt = fixedScriptWallet . BitGoPsbt . createEmpty ( networkName , rootWalletKeys ) ;
156+ }
157+
158+ unspents . forEach ( ( unspent ) => {
159+ const { txid, vout } = utxolib . bitgo . parseOutputId ( unspent . id ) ;
160+ const signPath : fixedScriptWallet . SignPath | undefined = isTaprootChain ( unspent . chain )
161+ ? { signer : 'user' , cosigner : 'backup' }
162+ : undefined ;
163+
164+ // prevTx may be added dynamically in backupKeyRecovery for non-segwit inputs
165+ const prevTx = ( unspent as WalletUnspent < bigint > & { prevTx ?: Buffer } ) . prevTx ;
65166
167+ wasmPsbt . addWalletInput (
168+ {
169+ txid,
170+ vout,
171+ value : unspent . value ,
172+ prevTx : prevTx ,
173+ } ,
174+ rootWalletKeys ,
175+ {
176+ scriptId : { chain : unspent . chain , index : unspent . index } ,
177+ signPath,
178+ }
179+ ) ;
180+ } ) ;
181+
182+ // Convert wasm-utxo PSBT to utxolib PSBT for dimension calculation and output addition
183+ const psbt = utxolib . bitgo . createPsbtFromBuffer ( Buffer . from ( wasmPsbt . serialize ( ) ) , network ) ;
184+
185+ let dimensions = Dimensions . fromPsbt ( psbt ) . plus (
186+ Dimensions . fromOutput ( { script : utxolib . address . toOutputScript ( recoveryDestination , network ) } )
187+ ) ;
188+
189+ if ( keyRecoveryServiceFeeAddress ) {
190+ dimensions = dimensions . plus (
191+ Dimensions . fromOutput ( {
192+ script : utxolib . address . toOutputScript ( keyRecoveryServiceFeeAddress , network ) ,
193+ } )
194+ ) ;
195+ }
196+
197+ const approximateFee = BigInt ( dimensions . getVSize ( ) * feeRateSatVB ) ;
198+ const totalInputAmount = utxolib . bitgo . unspentSum ( unspents , 'bigint' ) ;
66199 const recoveryAmount = totalInputAmount - approximateFee - keyRecoveryServiceFee ;
67200
68- // FIXME(BTC-2650): we should check for dust limit here instead
69201 if ( recoveryAmount < BigInt ( 0 ) ) {
70202 throw new InsufficientFundsError ( totalInputAmount , approximateFee , keyRecoveryServiceFee , recoveryAmount ) ;
71203 }
@@ -82,6 +214,33 @@ export function createBackupKeyRecoveryPsbt(
82214 return psbt ;
83215}
84216
217+ /**
218+ * Create a backup key recovery PSBT.
219+ *
220+ * @param network - The network for the PSBT
221+ * @param rootWalletKeys - The wallet keys
222+ * @param unspents - The unspents to include in the PSBT
223+ * @param options - Options for creating the PSBT
224+ * @param backend - Which backend to use for PSBT creation (default: 'wasm-utxo')
225+ */
226+ export function createBackupKeyRecoveryPsbt (
227+ network : utxolib . Network ,
228+ rootWalletKeys : RootWalletKeys ,
229+ unspents : WalletUnspent < bigint > [ ] ,
230+ options : CreateBackupKeyRecoveryPsbtOptions ,
231+ backend : PsbtBackend = 'wasm-utxo'
232+ ) : utxolib . bitgo . UtxoPsbt {
233+ if ( options . keyRecoveryServiceFee > 0 && ! options . keyRecoveryServiceFeeAddress ) {
234+ throw new Error ( 'keyRecoveryServiceFeeAddress is required when keyRecoveryServiceFee is provided' ) ;
235+ }
236+
237+ if ( backend === 'wasm-utxo' ) {
238+ return createBackupKeyRecoveryPsbtWasm ( network , rootWalletKeys , unspents , options ) ;
239+ } else {
240+ return createBackupKeyRecoveryPsbtUtxolib ( network , rootWalletKeys , unspents , options ) ;
241+ }
242+ }
243+
85244export function getRecoveryAmount ( psbt : utxolib . bitgo . UtxoPsbt , address : string ) : bigint {
86245 const recoveryOutputScript = utxolib . address . toOutputScript ( address , psbt . network ) ;
87246 const output = psbt . txOutputs . find ( ( o ) => o . script . equals ( recoveryOutputScript ) ) ;
0 commit comments