11import _ from 'lodash' ;
22import * as utxolib from '@bitgo/utxo-lib' ;
3- import { Dimensions } from '@bitgo/unspents' ;
43import {
54 BitGoBase ,
65 ErrorNoInputToRecover ,
@@ -20,6 +19,7 @@ import { generateAddressWithChainAndIndex } from '../address';
2019import { forCoin , RecoveryProvider } from './RecoveryProvider' ;
2120import { MempoolApi } from './mempoolApi' ;
2221import { CoingeckoApi } from './coingeckoApi' ;
22+ import { createBackupKeyRecoveryPsbt , getRecoveryAmount } from './psbt' ;
2323
2424type ScriptType2Of3 = utxolib . bitgo . outputScripts . ScriptType2Of3 ;
2525type ChainCode = utxolib . bitgo . ChainCode ;
@@ -31,6 +31,14 @@ type WalletUnspentJSON = utxolib.bitgo.WalletUnspent & {
3131
3232const { getInternalChainCode, scriptTypeForChain, outputScripts, getExternalChainCode } = utxolib . bitgo ;
3333
34+ // V1 only deals with BTC. 50 sat/vbyte is very arbitrary.
35+ export const DEFAULT_RECOVERY_FEERATE_SAT_VBYTE_V1 = 50 ;
36+
37+ // FIXME(BTC-2691): it is unclear why sweeps have a different default than regular recovery. 100 sat/vbyte is extremely high.
38+ export const DEFAULT_RECOVERY_FEERATE_SAT_VBYTE_V1_SWEEP = 100 ;
39+
40+ // FIXME(BTC-2691): it makes little sense to have a single default for every coin.
41+ export const DEFAULT_RECOVERY_FEERATE_SAT_VBYTE_V2 = 50 ;
3442export interface FormattedOfflineVaultTxInfo {
3543 txInfo : {
3644 unspents ?: WalletUnspentJSON [ ] ;
@@ -92,6 +100,7 @@ export interface RecoverParams {
92100 apiKey ?: string ;
93101 userKeyPath ?: string ;
94102 recoveryProvider ?: RecoveryProvider ;
103+ /** Satoshi per byte */
95104 feeRate ?: number ;
96105}
97106
@@ -323,27 +332,17 @@ export async function backupKeyRecovery(
323332 throw new ErrorNoInputToRecover ( ) ;
324333 }
325334
326- // Build the psbt
327- const psbt = utxolib . bitgo . createPsbtForNetwork ( { network : coin . network } ) ;
328- // xpubs can become handy for many things.
329- utxolib . bitgo . addXpubsToPsbt ( psbt , walletKeys ) ;
330335 const txInfo = { } as BackupKeyRecoveryTransansaction ;
331336 const feePerByte : number =
332- params . feeRate !== undefined ? params . feeRate : await getRecoveryFeePerBytes ( coin , { defaultValue : 50 } ) ;
333-
334- // KRS recovery transactions have a 2nd output to pay the recovery fee, like paygo fees.
335- const dimensions = Dimensions . fromPsbt ( psbt ) . plus ( isKrsRecovery ? Dimensions . SingleOutput . p2wsh : Dimensions . ZERO ) ;
336- const approximateFee = BigInt ( dimensions . getVSize ( ) * feePerByte ) ;
337+ params . feeRate !== undefined
338+ ? params . feeRate
339+ : await getRecoveryFeePerBytes ( coin , { defaultValue : DEFAULT_RECOVERY_FEERATE_SAT_VBYTE_V2 } ) ;
337340
338341 txInfo . inputs =
339342 responseTxFormat === 'legacy'
340343 ? unspents . map ( ( u ) => ( { ...u , value : Number ( u . value ) , valueString : u . value . toString ( ) , prevTx : undefined } ) )
341344 : undefined ;
342345
343- unspents . forEach ( ( unspent ) => {
344- utxolib . bitgo . addWalletUnspentToPsbt ( psbt , unspent , walletKeys , 'user' , 'backup' ) ;
345- } ) ;
346-
347346 let krsFee = BigInt ( 0 ) ;
348347 if ( isKrsRecovery && params . krsProvider ) {
349348 try {
@@ -354,33 +353,26 @@ export async function backupKeyRecovery(
354353 }
355354 }
356355
357- const recoveryAmount = totalInputAmount - approximateFee - krsFee ;
358-
359- if ( recoveryAmount < BigInt ( 0 ) ) {
360- throw new Error ( `this wallet\'s balance is too low to pay the fees specified by the KRS provider.
361- Existing balance on wallet: ${ totalInputAmount . toString ( ) } . Estimated network fee for the recovery transaction
362- : ${ approximateFee . toString ( ) } , KRS fee to pay: ${ krsFee . toString ( ) } . After deducting fees, your total
363- recoverable balance is ${ recoveryAmount . toString ( ) } ` ) ;
364- }
365-
366- const recoveryOutputScript = utxolib . address . toOutputScript ( params . recoveryDestination , coin . network ) ;
367- psbt . addOutput ( { script : recoveryOutputScript , value : recoveryAmount } ) ;
368-
356+ let krsFeeAddress : string | undefined ;
369357 if ( krsProvider && krsFee > BigInt ( 0 ) ) {
370358 if ( ! krsProvider . feeAddresses ) {
371359 throw new Error ( `keyProvider must define feeAddresses` ) ;
372360 }
373361
374- const krsFeeAddress = krsProvider . feeAddresses [ coin . getChain ( ) ] ;
362+ krsFeeAddress = krsProvider . feeAddresses [ coin . getChain ( ) ] ;
375363
376364 if ( ! krsFeeAddress ) {
377365 throw new Error ( 'this KRS provider has not configured their fee structure yet - recovery cannot be completed' ) ;
378366 }
379-
380- const krsFeeOutputScript = utxolib . address . toOutputScript ( krsFeeAddress , coin . network ) ;
381- psbt . addOutput ( { script : krsFeeOutputScript , value : krsFee } ) ;
382367 }
383368
369+ const psbt = createBackupKeyRecoveryPsbt ( coin . network , walletKeys , unspents , {
370+ feeRateSatVB : feePerByte ,
371+ recoveryDestination : params . recoveryDestination ,
372+ keyRecoveryServiceFee : krsFee ,
373+ keyRecoveryServiceFeeAddress : krsFeeAddress ,
374+ } ) ;
375+
384376 if ( isUnsignedSweep ) {
385377 return {
386378 txHex : psbt . toHex ( ) ,
@@ -408,6 +400,7 @@ export async function backupKeyRecovery(
408400 if ( isKrsRecovery ) {
409401 txInfo . coin = coin . getChain ( ) ;
410402 txInfo . backupKey = params . backupKey ;
403+ const recoveryAmount = getRecoveryAmount ( psbt , params . recoveryDestination ) ;
411404 txInfo . recoveryAmount = Number ( recoveryAmount ) ;
412405 txInfo . recoveryAmountString = recoveryAmount . toString ( ) ;
413406 }
@@ -446,7 +439,9 @@ export async function v1BackupKeyRecovery(
446439 throw new Error ( 'invalid recoveryDestination' ) ;
447440 }
448441
449- const recoveryFeePerByte = await getRecoveryFeePerBytes ( coin , { defaultValue : 50 } ) ;
442+ const recoveryFeePerByte = await getRecoveryFeePerBytes ( coin , {
443+ defaultValue : DEFAULT_RECOVERY_FEERATE_SAT_VBYTE_V1 ,
444+ } ) ;
450445 const v1wallet = await bitgo . wallets ( ) . get ( { id : params . walletId } ) ;
451446 return await v1wallet . recover ( {
452447 ...params ,
@@ -472,7 +467,9 @@ export async function v1Sweep(
472467
473468 let recoveryFeePerByte = 100 ;
474469 if ( bitgo . env === 'prod' ) {
475- recoveryFeePerByte = await getRecoveryFeePerBytes ( coin , { defaultValue : 100 } ) ;
470+ recoveryFeePerByte = await getRecoveryFeePerBytes ( coin , {
471+ defaultValue : DEFAULT_RECOVERY_FEERATE_SAT_VBYTE_V1_SWEEP ,
472+ } ) ;
476473 }
477474
478475 const v1wallet = await bitgo . wallets ( ) . get ( { id : params . walletId } ) ;
0 commit comments