Skip to content

Commit 5675ff4

Browse files
feat(mbe): fix rebase
1 parent 5393cef commit 5675ff4

2 files changed

Lines changed: 89 additions & 241 deletions

File tree

Lines changed: 46 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -1,260 +1,65 @@
1-
import { BaseCoin, BitGoAPI, MethodNotImplementedError } from 'bitgo';
2-
3-
import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth';
4-
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
5-
6-
import {
7-
isEddsaCoin,
8-
isEthLikeCoin,
9-
isFormattedOfflineVaultTxInfo,
10-
isUtxoCoin,
11-
} from '../../../shared/coinUtils';
12-
import {
13-
DEFAULT_MUSIG_ETH_GAS_PARAMS,
14-
getReplayProtectionOptions,
15-
} from '../../../shared/recoveryUtils';
16-
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
171
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
18-
import { recoverEddsaWallets } from './recoverEddsaWallets';
19-
import { EnvironmentName } from '../../../shared/types';
20-
import assert from 'assert';
21-
22-
interface RecoveryParams {
23-
userKey: string;
24-
backupKey: string;
25-
walletContractAddress: string;
26-
recoveryDestination: string;
27-
apiKey: string;
28-
}
29-
30-
interface EnclavedRecoveryParams {
31-
userPub: string;
32-
backupPub: string;
33-
apiKey: string;
34-
unsignedSweepPrebuildTx: any; // TODO: type this properly once we have the SDK types
35-
coinSpecificParams: any;
36-
walletContractAddress: string;
37-
}
38-
39-
async function handleEthLikeRecovery(
40-
sdkCoin: BaseCoin,
41-
commonRecoveryParams: RecoveryParams,
42-
enclavedExpressClient: any,
43-
params: EnclavedRecoveryParams,
44-
env: EnvironmentName,
45-
) {
46-
try {
47-
const { gasLimit, gasPrice, maxFeePerGas, maxPriorityFeePerGas } = DEFAULT_MUSIG_ETH_GAS_PARAMS;
48-
const unsignedSweepPrebuildTx = await (sdkCoin as AbstractEthLikeNewCoins).recover({
49-
...commonRecoveryParams,
50-
gasPrice,
51-
gasLimit,
52-
eip1559: {
53-
maxFeePerGas,
54-
maxPriorityFeePerGas,
55-
},
56-
replayProtectionOptions: getReplayProtectionOptions(env),
57-
});
58-
59-
const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({
60-
...params,
61-
unsignedSweepPrebuildTx,
62-
});
63-
64-
return fullSignedRecoveryTx;
65-
} catch (err) {
66-
throw err;
67-
}
68-
}
69-
70-
// function getKeyNonceFromParams(
71-
// coinSpecificParams: EnclavedRecoveryParams['coinSpecificParams'] | undefined,
72-
// ) {
73-
// // formatted as in WRW
74-
// if (!coinSpecificParams) return undefined;
75-
//
76-
// const { publicKeyNonce, secretKeyNonce } = coinSpecificParams;
77-
// if (!publicKeyNonce || !secretKeyNonce) return undefined;
78-
//
79-
// // coinSpecificParams is untyped so we need to cast the keys in order to avoid build errors.
80-
// return { publicKey: publicKeyNonce as string, secretKey: secretKeyNonce as string };
81-
// }
82-
83-
async function handleEddsaRecovery(
84-
bitgo: BitGoAPI,
85-
sdkCoin: BaseCoin,
86-
commonRecoveryParams: RecoveryParams,
87-
enclavedExpressClient: EnclavedExpressClient,
88-
params: EnclavedRecoveryParams,
89-
) {
90-
const { recoveryDestination, userKey } = commonRecoveryParams;
91-
try {
92-
const unsignedSweepPrebuildTx = await recoverEddsaWallets(bitgo, sdkCoin, {
93-
bitgoKey: userKey,
94-
recoveryDestination,
95-
apiKey: params.apiKey,
96-
});
97-
console.log('Unsigned sweep tx');
98-
console.log(JSON.stringify(unsignedSweepPrebuildTx, null, 2));
99-
100-
const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMPC({
101-
userPub: params.userPub,
102-
backupPub: params.backupPub,
103-
apiKey: params.apiKey,
104-
unsignedSweepPrebuildTx,
105-
coinSpecificParams: params.coinSpecificParams,
106-
walletContractAddress: params.walletContractAddress,
107-
});
108-
109-
return fullSignedRecoveryTx;
110-
} catch (err) {
111-
throw err;
112-
}
113-
}
114-
115-
export type UtxoCoinSpecificRecoveryParams = Pick<
116-
Parameters<AbstractUtxoCoin['recover']>[0],
117-
| 'apiKey'
118-
| 'userKey'
119-
| 'backupKey'
120-
| 'bitgoKey'
121-
| 'ignoreAddressTypes'
122-
| 'scan'
123-
| 'feeRate'
124-
| 'recoveryDestination'
125-
>;
126-
127-
async function handleUtxoLikeRecovery(
128-
sdkCoin: BaseCoin,
129-
enclavedClient: EnclavedExpressClient,
130-
recoveryParams: UtxoCoinSpecificRecoveryParams,
131-
): Promise<{ txHex: string }> {
132-
const abstractUtxoCoin = sdkCoin as unknown as AbstractUtxoCoin;
133-
const recoverTx = await abstractUtxoCoin.recover(recoveryParams);
134-
135-
console.log('UTXO recovery transaction created:', recoverTx);
136-
if (!isFormattedOfflineVaultTxInfo(recoverTx)) {
137-
throw new MethodNotImplementedError(`Unknown transaction ${JSON.stringify(recoverTx)} created`);
138-
}
139-
140-
return (await enclavedClient.recoveryMultisig({
141-
userPub: recoveryParams.userKey,
142-
backupPub: recoveryParams.backupKey,
143-
bitgoPub: recoveryParams.bitgoKey,
144-
unsignedSweepPrebuildTx: recoverTx,
145-
walletContractAddress: '',
146-
})) as { txHex: string };
147-
}
148-
149-
export async function handleRecoveryWalletOnPrem(
150-
req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>,
2+
import logger from '../../../logger';
3+
import { isSolCoin } from '../../../shared/coinUtils';
4+
import { MPCTx } from 'bitgo';
5+
import { RecoveryTransaction } from '@bitgo/sdk-coin-trx';
6+
7+
// Handler for recovery from receive addresses (consolidation sweeps)
8+
export async function handleRecoveryConsolidationsOnPrem(
9+
req: MasterApiSpecRouteRequest<'v1.wallet.recoveryConsolidations', 'post'>,
15110
) {
15211
const bitgo = req.bitgo;
15312
const coin = req.decoded.coin;
15413
const enclavedExpressClient = req.enclavedExpressClient;
155-
const { recoveryDestinationAddress, coinSpecificParams } = req.decoded;
156-
157-
const sdkCoin = bitgo.coin(coin);
158-
159-
// Handle TSS recovery
160-
if (req.decoded.isTssRecovery) {
161-
assert(req.decoded.tssRecoveryParams, 'TSS recovery parameters are required');
162-
const { commonKeychain } = req.decoded.tssRecoveryParams;
163-
if (!commonKeychain) {
164-
throw new Error('Common keychain is required for TSS recovery');
165-
}
166-
167-
if (isEddsaCoin(sdkCoin)) {
168-
return handleEddsaRecovery(
169-
req.bitgo,
170-
sdkCoin,
171-
{
172-
userKey: commonKeychain,
173-
backupKey: commonKeychain,
174-
walletContractAddress: '',
175-
recoveryDestination: recoveryDestinationAddress,
176-
apiKey: req.decoded.apiKey || '',
177-
},
178-
enclavedExpressClient,
179-
{
180-
userPub: commonKeychain,
181-
backupPub: commonKeychain,
182-
apiKey: '',
183-
walletContractAddress: '',
184-
unsignedSweepPrebuildTx: undefined,
185-
coinSpecificParams: undefined,
186-
},
187-
);
188-
} else {
189-
throw new MethodNotImplementedError(
190-
`TSS recovery is not implemented for coin: ${coin}. Supported coins are Eddsa coins.`,
191-
);
192-
}
193-
}
19414

195-
// Handle standard recovery
196-
if (!req.decoded.multiSigRecoveryParams) {
197-
throw new Error('MultiSig recovery parameters are required for standard recovery');
198-
}
199-
200-
const { userPub, backupPub, bitgoPub, walletContractAddress } =
201-
req.decoded.multiSigRecoveryParams;
202-
const apiKey = req.decoded.apiKey || '';
15+
const { userPub, backupPub, bitgoPub } = req.decoded;
20316

204-
if (!userPub || !backupPub) {
205-
throw new Error('Missing required fields for standard recovery');
17+
const sdkCoin = bitgo.coin(coin);
18+
let txs: MPCTx[] | RecoveryTransaction[] = [];
19+
// 1. Build unsigned consolidations
20+
if (isSolCoin(sdkCoin) && !req.decoded.durableNonces) {
21+
throw new Error('durableNonces is required for Solana consolidation recovery');
20622
}
20723

208-
// Check if the public key is valid
209-
if (!sdkCoin.isValidPub(userPub)) {
210-
throw new Error('Invalid user public key format');
211-
} else if (!sdkCoin.isValidPub(backupPub)) {
212-
throw new Error('Invalid backup public key format');
24+
if (typeof (sdkCoin as any).recoverConsolidations !== 'function') {
25+
throw new Error(`recoverConsolidations is not supported for coin: ${coin}`);
21326
}
21427

215-
const commonRecoveryParams: RecoveryParams = {
28+
// Use type assertion to access recoverConsolidations
29+
const result = await (sdkCoin as any).recoverConsolidations({
30+
...req.decoded,
21631
userKey: userPub,
21732
backupKey: backupPub,
218-
walletContractAddress,
219-
recoveryDestination: recoveryDestinationAddress,
220-
apiKey,
221-
};
33+
bitgoKey: bitgoPub,
34+
durableNonces: req.decoded.durableNonces,
35+
});
36+
37+
if ('transactions' in result) {
38+
txs = result.transactions;
39+
} else if ('txRequests' in result) {
40+
txs = result.txRequests;
41+
} else {
42+
throw new Error('recoverConsolidations did not return expected transactions');
43+
}
22244

223-
if (isEthLikeCoin(sdkCoin)) {
224-
if (!walletContractAddress) {
225-
throw new Error('Missing walletContract address');
226-
}
227-
return handleEthLikeRecovery(
228-
sdkCoin,
229-
commonRecoveryParams,
230-
enclavedExpressClient,
231-
{
45+
logger.debug(`Found ${txs.length} unsigned consolidation transactions`);
46+
47+
// 2. For each unsigned sweep, get it signed by EBE (using recoveryMultisig)
48+
const signedTxs = [];
49+
try {
50+
for (const tx of txs) {
51+
const signedTx = await enclavedExpressClient.recoveryMultisig({
23252
userPub,
23353
backupPub,
234-
apiKey,
235-
unsignedSweepPrebuildTx: undefined,
236-
coinSpecificParams: undefined,
237-
walletContractAddress,
238-
},
239-
bitgo.env as EnvironmentName,
240-
);
241-
}
242-
if (!bitgoPub) {
243-
throw new Error('BitGo public key is required for recovery');
244-
}
54+
unsignedSweepPrebuildTx: tx,
55+
walletContractAddress: '',
56+
});
57+
signedTxs.push(signedTx);
58+
}
24559

246-
if (isUtxoCoin(sdkCoin)) {
247-
return handleUtxoLikeRecovery(sdkCoin, req.enclavedExpressClient, {
248-
userKey: userPub,
249-
backupKey: backupPub,
250-
bitgoKey: bitgoPub,
251-
ignoreAddressTypes: coinSpecificParams?.ignoreAddressTypes ?? [],
252-
scan: coinSpecificParams?.addressScan,
253-
feeRate: coinSpecificParams?.feeRate,
254-
recoveryDestination: recoveryDestinationAddress,
255-
apiKey,
256-
});
60+
return { signedTxs };
61+
} catch (err) {
62+
logger.error('Error during consolidation recovery:', err);
63+
throw err;
25764
}
258-
259-
throw new MethodNotImplementedError('Recovery wallet is not supported for this coin: ' + coin);
26065
}

src/api/master/routers/masterApiSpec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { handleRecoveryWalletOnPrem } from '../handlers/recoveryWallet';
2424
import { handleConsolidate } from '../handlers/handleConsolidate';
2525
import { handleAccelerate } from '../handlers/handleAccelerate';
2626
import { handleConsolidateUnspents } from '../handlers/handleConsolidateUnspents';
27+
import { handleRecoveryConsolidationsOnPrem } from '../handlers/recoveryConsolidationsWallet';
2728

2829
// Middleware functions
2930
export function parseBody(req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -197,6 +198,26 @@ const RecoveryWalletRequest = {
197198

198199
export type RecoveryWalletRequest = typeof RecoveryWalletRequest;
199200

201+
const RecoveryConsolidationsWalletRequest = {
202+
userPub: t.string,
203+
backupPub: t.string,
204+
bitgoPub: t.union([t.undefined, t.string]),
205+
walletContractAddress: t.string,
206+
recoveryDestinationAddress: t.string,
207+
apiKey: t.string,
208+
durableNonces: t.union([t.undefined, t.boolean]),
209+
};
210+
211+
// Response type for /recoveryconsolidations endpoint
212+
const RecoveryConsolidationsWalletResponse: HttpResponse = {
213+
200: t.any,
214+
500: t.type({
215+
error: t.string,
216+
details: t.string,
217+
}),
218+
};
219+
220+
200221
export const ConsolidateUnspentsRequest = {
201222
pubkey: t.string,
202223
source: t.union([t.literal('user'), t.literal('backup')]),
@@ -273,6 +294,20 @@ export const MasterApiSpec = apiSpec({
273294
description: 'Recover an existing wallet',
274295
}),
275296
},
297+
'v1.wallet.recoveryConsolidations': {
298+
post: httpRoute({
299+
method: 'POST',
300+
path: '/api/{coin}/wallet/recoveryconsolidations',
301+
request: httpRequest({
302+
params: {
303+
coin: t.string,
304+
},
305+
body: RecoveryConsolidationsWalletRequest,
306+
}),
307+
response: RecoveryConsolidationsWalletResponse,
308+
description: 'Consolidate and recover an existing wallet',
309+
}),
310+
},
276311
'v1.wallet.consolidate': {
277312
post: httpRoute({
278313
method: 'POST',
@@ -378,6 +413,14 @@ export function createMasterApiRouter(
378413
}),
379414
]);
380415

416+
router.post('v1.wallet.recoveryConsolidations', [
417+
responseHandler<MasterExpressConfig>(async (req: express.Request) => {
418+
const typedReq = req as GenericMasterApiSpecRouteRequest;
419+
const result = await handleRecoveryConsolidationsOnPrem(typedReq);
420+
return Response.ok(result);
421+
}),
422+
]);
423+
381424
router.post('v1.wallet.accelerate', [
382425
responseHandler<MasterExpressConfig>(async (req: express.Request) => {
383426
const typedReq = req as GenericMasterApiSpecRouteRequest;

0 commit comments

Comments
 (0)