Skip to content

Commit ae3c69b

Browse files
authored
Merge pull request #51 from BitGo/WP-5185
feat: raise error on no recipients eth musig rec and some refactors
2 parents 2538178 + a98ce5e commit ae3c69b

File tree

3 files changed

+63
-28
lines changed

3 files changed

+63
-28
lines changed

src/api/enclaved/handlers/recoveryMultisigTransaction.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { SignFinalOptions } from '@bitgo/abstract-eth';
2-
import { HalfSignedUtxoTransaction, MethodNotImplementedError } from 'bitgo';
2+
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
3+
import { HalfSignedUtxoTransaction, MethodNotImplementedError, TransactionRecipient } from 'bitgo';
34
import { EnclavedApiSpecRouteRequest } from '../../../enclavedBitgoExpress/routers/enclavedApiSpec';
5+
import { EnvironmentName } from '../../../initConfig';
46
import logger from '../../../logger';
57
import {
68
isEthLikeCoin,
@@ -9,12 +11,11 @@ import {
911
} from '../../../shared/coinUtils';
1012
import {
1113
addEthLikeRecoveryExtras,
12-
getDefaultMusigEthGasParams,
14+
DEFAULT_MUSIG_ETH_GAS_PARAMS,
1315
getReplayProtectionOptions,
1416
} from '../../../shared/recoveryUtils';
1517
import { SignedEthLikeRecoveryTx } from '../../../types/transaction';
1618
import { retrieveKmsPrvKey } from '../utils';
17-
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
1819

1920
export async function recoveryMultisigTransaction(
2021
req: EnclavedApiSpecRouteRequest<'v1.multisig.recovery', 'post'>,
@@ -43,9 +44,13 @@ export async function recoveryMultisigTransaction(
4344
const walletKeys = unsignedSweepPrebuildTx.xpubxWithDerivationPath;
4445
const pubs = [walletKeys?.user?.xpub, walletKeys?.backup?.xpub, walletKeys?.bitgo?.xpub];
4546
const { gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas } =
46-
getDefaultMusigEthGasParams();
47+
DEFAULT_MUSIG_ETH_GAS_PARAMS;
4748

4849
try {
50+
checkIfNoRecipients({
51+
recipients: unsignedSweepPrebuildTx.recipients,
52+
coin: req.decoded.coin,
53+
});
4954
const halfSignedTxBase = await baseCoin.signTransaction({
5055
isLastSignature: false,
5156
prv: userPrv,
@@ -61,6 +66,7 @@ export async function recoveryMultisigTransaction(
6166
maxPriorityFeePerGas,
6267
},
6368
replayProtectionOptions: getReplayProtectionOptions(
69+
bitgo.env as EnvironmentName,
6470
unsignedSweepPrebuildTx.replayProtectionOptions,
6571
),
6672
txPrebuild: {
@@ -72,13 +78,15 @@ export async function recoveryMultisigTransaction(
7278
maxPriorityFeePerGas,
7379
},
7480
replayProtectionOptions: getReplayProtectionOptions(
81+
bitgo.env as EnvironmentName,
7582
unsignedSweepPrebuildTx.replayProtectionOptions,
7683
),
7784
},
7885
walletContractAddress,
7986
});
8087

8188
const halfSignedTx = addEthLikeRecoveryExtras({
89+
env: bitgo.env as EnvironmentName,
8290
signedTx: halfSignedTxBase as SignedEthLikeRecoveryTx,
8391
transaction: unsignedSweepPrebuildTx,
8492
isLastSignature: false,
@@ -108,6 +116,7 @@ export async function recoveryMultisigTransaction(
108116
maxPriorityFeePerGas,
109117
},
110118
replayProtectionOptions: getReplayProtectionOptions(
119+
bitgo.env as EnvironmentName,
111120
halfSignedTx?.replayProtectionOptions,
112121
),
113122
} as unknown as SignFinalOptions,
@@ -159,3 +168,17 @@ export async function recoveryMultisigTransaction(
159168
throw new MethodNotImplementedError('Unsupported coin type for recovery: ' + baseCoin);
160169
}
161170
}
171+
172+
function checkIfNoRecipients({
173+
recipients,
174+
coin,
175+
}: {
176+
recipients?: TransactionRecipient[];
177+
coin: string;
178+
}) {
179+
if (!recipients || recipients.length === 0) {
180+
const errorMsg = `Recovery tx for coin ${coin} must have at least one recipient.`;
181+
logger.error(errorMsg);
182+
throw new Error(errorMsg);
183+
}
184+
}

src/api/master/handlers/recoveryWallet.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import { BaseCoin, MethodNotImplementedError } from 'bitgo';
22

3-
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
43
import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth';
4+
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
55

66
import {
77
isEthLikeCoin,
88
isFormattedOfflineVaultTxInfo,
99
isUtxoCoin,
1010
} from '../../../shared/coinUtils';
1111
import {
12-
getDefaultMusigEthGasParams,
12+
DEFAULT_MUSIG_ETH_GAS_PARAMS,
1313
getReplayProtectionOptions,
1414
} from '../../../shared/recoveryUtils';
15-
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
15+
import { EnvironmentName } from '../../../shared/types/index';
1616
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
17+
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
1718

1819
interface RecoveryParams {
1920
userKey: string;
@@ -37,10 +38,10 @@ async function handleEthLikeRecovery(
3738
commonRecoveryParams: RecoveryParams,
3839
enclavedExpressClient: any,
3940
params: EnclavedRecoveryParams,
41+
env: EnvironmentName,
4042
) {
4143
try {
42-
const { gasLimit, gasPrice, maxFeePerGas, maxPriorityFeePerGas } =
43-
getDefaultMusigEthGasParams();
44+
const { gasLimit, gasPrice, maxFeePerGas, maxPriorityFeePerGas } = DEFAULT_MUSIG_ETH_GAS_PARAMS;
4445
const unsignedSweepPrebuildTx = await (sdkCoin as AbstractEthLikeNewCoins).recover({
4546
...commonRecoveryParams,
4647
gasPrice,
@@ -49,7 +50,7 @@ async function handleEthLikeRecovery(
4950
maxFeePerGas,
5051
maxPriorityFeePerGas,
5152
},
52-
replayProtectionOptions: getReplayProtectionOptions(),
53+
replayProtectionOptions: getReplayProtectionOptions(env),
5354
});
5455

5556
const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({
@@ -133,14 +134,20 @@ export async function handleRecoveryWalletOnPrem(
133134
}
134135

135136
if (isEthLikeCoin(sdkCoin)) {
136-
return handleEthLikeRecovery(sdkCoin, commonRecoveryParams, enclavedExpressClient, {
137-
userPub,
138-
backupPub,
139-
apiKey,
140-
unsignedSweepPrebuildTx: undefined,
141-
coinSpecificParams: undefined,
142-
walletContractAddress,
143-
});
137+
return handleEthLikeRecovery(
138+
sdkCoin,
139+
commonRecoveryParams,
140+
enclavedExpressClient,
141+
{
142+
userPub,
143+
backupPub,
144+
apiKey,
145+
unsignedSweepPrebuildTx: undefined,
146+
coinSpecificParams: undefined,
147+
walletContractAddress,
148+
},
149+
bitgo.env as EnvironmentName,
150+
);
144151
}
145152
if (!bitgoPub) {
146153
throw new Error('BitGo public key is required for recovery');

src/shared/recoveryUtils.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import { type EnvironmentName } from '../shared/types/index';
12
import type { ReplayProtectionOptions, SignedEthLikeRecoveryTx } from '../types/transaction';
23

34
export function addEthLikeRecoveryExtras({
5+
env,
46
signedTx,
57
transaction,
68
isLastSignature,
79
replayProtectionOptions,
810
}: {
11+
env: EnvironmentName;
912
signedTx: SignedEthLikeRecoveryTx;
1013
transaction: any; // Same type as UnsignedSweepPrebuildTx
1114
isLastSignature: boolean;
@@ -50,28 +53,30 @@ export function addEthLikeRecoveryExtras({
5053
? transaction.backupKeyNonce
5154
: transaction.nextContractSequenceId;
5255
decoratedSignedTx.walletContractAddress = transaction.walletContractAddress;
53-
decoratedSignedTx.replayProtectionOptions = getReplayProtectionOptions(replayProtectionOptions);
56+
decoratedSignedTx.replayProtectionOptions = getReplayProtectionOptions(
57+
env,
58+
replayProtectionOptions,
59+
);
5460
}
5561

5662
return decoratedSignedTx;
5763
}
5864

5965
export function getReplayProtectionOptions(
66+
env: EnvironmentName,
6067
replayProtectionOptions: ReplayProtectionOptions | undefined = undefined,
6168
): ReplayProtectionOptions {
6269
return (
6370
replayProtectionOptions ?? {
64-
chain: 17000, // 1 if mainnet, 17000 if testnet
71+
chain: env === 'prod' ? 1 : 17000,
6572
hardfork: 'london',
6673
}
6774
);
6875
}
6976

70-
export function getDefaultMusigEthGasParams() {
71-
return {
72-
gasPrice: 20000000000,
73-
gasLimit: 200000,
74-
maxFeePerGas: 20000000000,
75-
maxPriorityFeePerGas: 10000000000,
76-
};
77-
}
77+
export const DEFAULT_MUSIG_ETH_GAS_PARAMS = {
78+
gasPrice: 20000000000,
79+
gasLimit: 200000,
80+
maxFeePerGas: 20000000000,
81+
maxPriorityFeePerGas: 10000000000,
82+
};

0 commit comments

Comments
 (0)