Skip to content

Commit 7ee0805

Browse files
feat: add durable nonce for sol recovery
1 parent e7fc6bc commit 7ee0805

File tree

3 files changed

+258
-65
lines changed

3 files changed

+258
-65
lines changed

masterBitgoExpress.json

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -719,23 +719,107 @@
719719
"coinSpecificParams": {
720720
"type": "object",
721721
"properties": {
722-
"addressScan": {
723-
"type": "number"
722+
"evmRecoveryOptions": {
723+
"type": "object",
724+
"properties": {
725+
"eip1559": {
726+
"type": "object",
727+
"properties": {
728+
"maxFeePerGas": {
729+
"type": "number"
730+
},
731+
"maxPriorityFeePerGas": {
732+
"type": "number"
733+
}
734+
},
735+
"required": [
736+
"maxFeePerGas",
737+
"maxPriorityFeePerGas"
738+
]
739+
},
740+
"gasLimit": {
741+
"type": "number"
742+
},
743+
"gasPrice": {
744+
"type": "number"
745+
},
746+
"replayProtectionOptions": {
747+
"type": "object",
748+
"properties": {
749+
"chain": {
750+
"oneOf": [
751+
{
752+
"type": "string"
753+
},
754+
{
755+
"type": "number"
756+
}
757+
]
758+
},
759+
"hardfork": {
760+
"type": "string"
761+
}
762+
},
763+
"required": [
764+
"chain",
765+
"hardfork"
766+
]
767+
},
768+
"scan": {
769+
"type": "number"
770+
}
771+
}
724772
},
725-
"feeRate": {
726-
"type": "number"
773+
"sol": {
774+
"type": "object",
775+
"properties": {
776+
"closeAtaAddress": {
777+
"type": "string"
778+
},
779+
"durableNonce": {
780+
"type": "object",
781+
"properties": {
782+
"publicKey": {
783+
"type": "string"
784+
},
785+
"secretKey": {
786+
"type": "string"
787+
}
788+
},
789+
"required": [
790+
"publicKey",
791+
"secretKey"
792+
]
793+
},
794+
"programId": {
795+
"type": "string"
796+
},
797+
"recoveryDestinationAtaAddress": {
798+
"type": "string"
799+
},
800+
"tokenContractAddress": {
801+
"type": "string"
802+
}
803+
}
727804
},
728-
"ignoreAddressTypes": {
729-
"type": "array",
730-
"items": {
731-
"type": "string",
732-
"enum": [
733-
"p2sh",
734-
"p2shP2wsh",
735-
"p2wsh",
736-
"p2tr",
737-
"p2trMusig2"
738-
]
805+
"utxo": {
806+
"type": "object",
807+
"properties": {
808+
"feeRate": {
809+
"type": "number"
810+
},
811+
"ignoreAddressTypes": {
812+
"type": "array",
813+
"items": {
814+
"type": "string"
815+
}
816+
},
817+
"scan": {
818+
"type": "number"
819+
},
820+
"userKeyPath": {
821+
"type": "string"
822+
}
739823
}
740824
}
741825
}

src/api/master/handlers/recoveryWallet.ts

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
import { BaseCoin, BitGoAPI, MethodNotImplementedError } from 'bitgo';
1+
import { BaseCoin, BitGoAPI, MethodNotImplementedError, MPCRecoveryOptions } from 'bitgo';
22

33
import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth';
44
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
55

66
import assert from 'assert';
77

8-
import {
9-
isEddsaCoin,
10-
isEthLikeCoin,
11-
isFormattedOfflineVaultTxInfo,
12-
isUtxoCoin,
13-
} from '../../../shared/coinUtils';
14-
import {
15-
DEFAULT_MUSIG_ETH_GAS_PARAMS,
16-
getReplayProtectionOptions,
17-
} from '../../../shared/recoveryUtils';
8+
import { isEddsaCoin, isEthLikeCoin, isFormattedOfflineVaultTxInfo, isUtxoCoin } from '../../../shared/coinUtils';
9+
import { DEFAULT_MUSIG_ETH_GAS_PARAMS, getReplayProtectionOptions } from '../../../shared/recoveryUtils';
10+
1811
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
19-
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
12+
import {
13+
EvmRecoveryOptions,
14+
MasterApiSpecRouteRequest,
15+
ScriptType2Of3,
16+
SolanaRecoveryOptions,
17+
UtxoRecoveryOptions,
18+
} from '../routers/masterApiSpec';
2019
import { recoverEddsaWallets } from './recoverEddsaWallets';
2120
import { EnvironmentName } from '../../../shared/types';
2221
import logger from '../../../logger';
22+
import { CoinFamily } from '@bitgo/statics';
23+
import { type SolRecoveryOptions } from '@bitgo/sdk-coin-sol';
2324

2425
interface RecoveryParams {
2526
userKey: string;
@@ -34,17 +35,77 @@ interface EnclavedRecoveryParams {
3435
backupPub: string;
3536
apiKey: string;
3637
unsignedSweepPrebuildTx: any; // TODO: type this properly once we have the SDK types
37-
coinSpecificParams?: Record<string, undefined>;
38+
coinSpecificParams?: EvmRecoveryOptions | UtxoRecoveryOptions | SolanaRecoveryOptions;
3839
walletContractAddress: string;
3940
}
4041

42+
// Type guards for recovery options
43+
function isUtxoRecoveryOptions(params: any): params is UtxoRecoveryOptions {
44+
return (
45+
params &&
46+
(params.ignoreAddressTypes !== undefined ||
47+
params.userKeyPath !== undefined ||
48+
params.feeRate !== undefined)
49+
);
50+
}
51+
52+
function isEthRecoveryOptions(params: any): params is EvmRecoveryOptions {
53+
return (
54+
params &&
55+
(params.gasPrice !== undefined ||
56+
params.gasLimit !== undefined ||
57+
params.eip1559 !== undefined ||
58+
params.replayProtectionOptions !== undefined)
59+
);
60+
}
61+
62+
function isSolanaRecoveryOptions(params: any): params is SolanaRecoveryOptions {
63+
return (
64+
params &&
65+
(params.tokenContractAddress !== undefined ||
66+
params.closeAtaAddress !== undefined ||
67+
params.recoveryDestinationAtaAddress !== undefined ||
68+
params.programId !== undefined)
69+
);
70+
}
71+
72+
// Validation function to ensure correct params are used with correct coin types
73+
function validateRecoveryParams(
74+
sdkCoin: BaseCoin,
75+
params?: EvmRecoveryOptions | UtxoRecoveryOptions | SolanaRecoveryOptions,
76+
) {
77+
if (!params) {
78+
return;
79+
}
80+
81+
if (isUtxoCoin(sdkCoin)) {
82+
if (isEthRecoveryOptions(params) || isSolanaRecoveryOptions(params)) {
83+
throw new Error('Invalid parameters provided for UTXO coin recovery');
84+
}
85+
}
86+
87+
if (isEthLikeCoin(sdkCoin)) {
88+
if (isUtxoRecoveryOptions(params) || isSolanaRecoveryOptions(params)) {
89+
throw new Error('Invalid parameters provided for ETH-like coin recovery');
90+
}
91+
}
92+
93+
if (isEddsaCoin(sdkCoin)) {
94+
if (isUtxoRecoveryOptions(params) || isEthRecoveryOptions(params)) {
95+
throw new Error('Invalid parameters provided for Solana coin recovery');
96+
}
97+
}
98+
}
99+
41100
async function handleEthLikeRecovery(
42101
sdkCoin: BaseCoin,
43102
commonRecoveryParams: RecoveryParams,
44103
enclavedExpressClient: any,
45104
params: EnclavedRecoveryParams,
46105
env: EnvironmentName,
47106
) {
107+
// Validate that we have correct parameters for ETH recovery
108+
validateRecoveryParams(sdkCoin, params.coinSpecificParams);
48109
try {
49110
const { gasLimit, gasPrice, maxFeePerGas, maxPriorityFeePerGas } = DEFAULT_MUSIG_ETH_GAS_PARAMS;
50111
const unsignedSweepPrebuildTx = await (sdkCoin as AbstractEthLikeNewCoins).recover({
@@ -76,13 +137,34 @@ async function handleEddsaRecovery(
76137
enclavedExpressClient: EnclavedExpressClient,
77138
params: EnclavedRecoveryParams,
78139
) {
140+
// Validate that we have the correct parameters for Solana recovery
141+
validateRecoveryParams(sdkCoin, params.coinSpecificParams);
79142
const { recoveryDestination, userKey } = commonRecoveryParams;
80143
try {
81-
const unsignedSweepPrebuildTx = await recoverEddsaWallets(bitgo, sdkCoin, {
144+
const options: MPCRecoveryOptions = {
82145
bitgoKey: userKey,
83146
recoveryDestination,
84147
apiKey: params.apiKey,
85-
});
148+
};
149+
let unsignedSweepPrebuildTx: Awaited<ReturnType<typeof recoverEddsaWallets>>;
150+
if (sdkCoin.getFamily() === CoinFamily.SOL) {
151+
const solanaParams = params.coinSpecificParams as SolanaRecoveryOptions;
152+
console.log(params);
153+
const solanaRecoveryOptions: SolRecoveryOptions = {...options}
154+
solanaRecoveryOptions.recoveryDestinationAtaAddress = solanaParams.recoveryDestinationAtaAddress;
155+
solanaRecoveryOptions.closeAtaAddress = solanaParams.closeAtaAddress;
156+
solanaRecoveryOptions.tokenContractAddress = solanaParams.tokenContractAddress;
157+
solanaRecoveryOptions.programId = solanaParams.programId;
158+
if (solanaParams.durableNonce) {
159+
solanaRecoveryOptions.durableNonce = {
160+
publicKey: solanaParams.durableNonce.publicKey,
161+
secretKey: solanaParams.durableNonce.secretKey,
162+
};
163+
}
164+
unsignedSweepPrebuildTx = await recoverEddsaWallets(bitgo, sdkCoin, solanaRecoveryOptions);
165+
} else {
166+
unsignedSweepPrebuildTx = await recoverEddsaWallets(bitgo, sdkCoin, options);
167+
}
86168
logger.info('Unsigned sweep tx: ', JSON.stringify(unsignedSweepPrebuildTx, null, 2));
87169

88170
return await enclavedExpressClient.recoveryMPC({
@@ -168,7 +250,7 @@ export async function handleRecoveryWalletOnPrem(
168250
apiKey: '',
169251
walletContractAddress: '',
170252
unsignedSweepPrebuildTx: undefined,
171-
coinSpecificParams: undefined,
253+
coinSpecificParams: coinSpecificParams?.sol,
172254
},
173255
);
174256
} else {
@@ -219,7 +301,7 @@ export async function handleRecoveryWalletOnPrem(
219301
backupPub,
220302
apiKey,
221303
unsignedSweepPrebuildTx: undefined,
222-
coinSpecificParams: undefined,
304+
coinSpecificParams: coinSpecificParams?.evmRecoveryOptions,
223305
walletContractAddress,
224306
},
225307
bitgo.env as EnvironmentName,
@@ -234,9 +316,9 @@ export async function handleRecoveryWalletOnPrem(
234316
userKey: userPub,
235317
backupKey: backupPub,
236318
bitgoKey: bitgoPub,
237-
ignoreAddressTypes: coinSpecificParams?.ignoreAddressTypes ?? [],
238-
scan: coinSpecificParams?.addressScan,
239-
feeRate: coinSpecificParams?.feeRate,
319+
ignoreAddressTypes: (coinSpecificParams?.utxo?.ignoreAddressTypes as ScriptType2Of3[]) ?? [],
320+
scan: coinSpecificParams?.utxo?.scan,
321+
feeRate: coinSpecificParams?.utxo?.feeRate,
240322
recoveryDestination: recoveryDestinationAddress,
241323
apiKey,
242324
});

0 commit comments

Comments
 (0)