Skip to content

Commit acaef72

Browse files
feat(mbe): fix rebase
1 parent 00a4209 commit acaef72

2 files changed

Lines changed: 91 additions & 153 deletions

File tree

Lines changed: 48 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,170 +1,65 @@
1-
import { BaseCoin, MethodNotImplementedError } from 'bitgo';
2-
3-
import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth';
4-
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
5-
6-
import {
7-
isEthLikeCoin,
8-
isFormattedOfflineVaultTxInfo,
9-
isUtxoCoin,
10-
} from '../../../shared/coinUtils';
11-
import {
12-
DEFAULT_MUSIG_ETH_GAS_PARAMS,
13-
getReplayProtectionOptions,
14-
} from '../../../shared/recoveryUtils';
15-
import { EnvironmentName } from '../../../shared/types/index';
16-
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
171
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
18-
19-
interface RecoveryParams {
20-
userKey: string;
21-
backupKey: string;
22-
walletContractAddress: string;
23-
recoveryDestination: string;
24-
apiKey: string;
25-
}
26-
27-
interface EnclavedRecoveryParams {
28-
userPub: string;
29-
backupPub: string;
30-
apiKey: string;
31-
unsignedSweepPrebuildTx: any; // TODO: type this properly once we have the SDK types
32-
coinSpecificParams: any;
33-
walletContractAddress: string;
34-
}
35-
36-
async function handleEthLikeRecovery(
37-
sdkCoin: BaseCoin,
38-
commonRecoveryParams: RecoveryParams,
39-
enclavedExpressClient: any,
40-
params: EnclavedRecoveryParams,
41-
env: EnvironmentName,
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'>,
4210
) {
43-
try {
44-
const { gasLimit, gasPrice, maxFeePerGas, maxPriorityFeePerGas } = DEFAULT_MUSIG_ETH_GAS_PARAMS;
45-
const unsignedSweepPrebuildTx = await (sdkCoin as AbstractEthLikeNewCoins).recover({
46-
...commonRecoveryParams,
47-
gasPrice,
48-
gasLimit,
49-
eip1559: {
50-
maxFeePerGas,
51-
maxPriorityFeePerGas,
52-
},
53-
replayProtectionOptions: getReplayProtectionOptions(env),
54-
});
11+
const bitgo = req.bitgo;
12+
const coin = req.decoded.coin;
13+
const enclavedExpressClient = req.enclavedExpressClient;
5514

56-
const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({
57-
...params,
58-
unsignedSweepPrebuildTx,
59-
});
15+
const { userPub, backupPub, bitgoPub } = req.decoded;
6016

61-
return fullSignedRecoveryTx;
62-
} catch (err) {
63-
throw err;
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');
6422
}
65-
}
66-
67-
export type UtxoCoinSpecificRecoveryParams = Pick<
68-
Parameters<AbstractUtxoCoin['recover']>[0],
69-
| 'apiKey'
70-
| 'userKey'
71-
| 'backupKey'
72-
| 'bitgoKey'
73-
| 'ignoreAddressTypes'
74-
| 'scan'
75-
| 'feeRate'
76-
| 'recoveryDestination'
77-
>;
7823

79-
async function handleUtxoLikeRecovery(
80-
sdkCoin: BaseCoin,
81-
enclavedClient: EnclavedExpressClient,
82-
recoveryParams: UtxoCoinSpecificRecoveryParams,
83-
): Promise<{ txHex: string }> {
84-
const abstractUtxoCoin = sdkCoin as unknown as AbstractUtxoCoin;
85-
const recoverTx = await abstractUtxoCoin.recover(recoveryParams);
86-
87-
console.log('UTXO recovery transaction created:', recoverTx);
88-
if (!isFormattedOfflineVaultTxInfo(recoverTx)) {
89-
throw new MethodNotImplementedError(`Unknown transaction ${JSON.stringify(recoverTx)} created`);
24+
if (typeof (sdkCoin as any).recoverConsolidations !== 'function') {
25+
throw new Error(`recoverConsolidations is not supported for coin: ${coin}`);
9026
}
9127

92-
return (await enclavedClient.recoveryMultisig({
93-
userPub: recoveryParams.userKey,
94-
backupPub: recoveryParams.backupKey,
95-
bitgoPub: recoveryParams.bitgoKey,
96-
unsignedSweepPrebuildTx: recoverTx,
97-
walletContractAddress: '',
98-
})) as { txHex: string };
99-
}
100-
101-
export async function handleRecoveryWalletOnPrem(
102-
req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>,
103-
) {
104-
const bitgo = req.bitgo;
105-
const coin = req.decoded.coin;
106-
const enclavedExpressClient = req.enclavedExpressClient;
107-
108-
const {
109-
userPub,
110-
backupPub,
111-
bitgoPub,
112-
walletContractAddress,
113-
recoveryDestinationAddress,
114-
coinSpecificParams,
115-
apiKey,
116-
} = req.decoded;
117-
118-
//construct a common payload for the recovery that it's repeated in any kind of recovery
119-
const commonRecoveryParams: RecoveryParams = {
28+
// Use type assertion to access recoverConsolidations
29+
const result = await (sdkCoin as any).recoverConsolidations({
30+
...req.decoded,
12031
userKey: userPub,
12132
backupKey: backupPub,
122-
walletContractAddress,
123-
recoveryDestination: recoveryDestinationAddress,
124-
apiKey,
125-
};
126-
127-
const sdkCoin = bitgo.coin(coin);
128-
129-
// Check if the public key is valid
130-
if (!sdkCoin.isValidPub(userPub)) {
131-
throw new Error('Invalid user public key format');
132-
} else if (!sdkCoin.isValidPub(backupPub)) {
133-
throw new Error('Invalid backup public');
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');
13443
}
13544

136-
if (isEthLikeCoin(sdkCoin)) {
137-
return handleEthLikeRecovery(
138-
sdkCoin,
139-
commonRecoveryParams,
140-
enclavedExpressClient,
141-
{
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({
14252
userPub,
14353
backupPub,
144-
apiKey,
145-
unsignedSweepPrebuildTx: undefined,
146-
coinSpecificParams: undefined,
147-
walletContractAddress,
148-
},
149-
bitgo.env as EnvironmentName,
150-
);
151-
}
152-
if (!bitgoPub) {
153-
throw new Error('BitGo public key is required for recovery');
154-
}
54+
unsignedSweepPrebuildTx: tx,
55+
walletContractAddress: '',
56+
});
57+
signedTxs.push(signedTx);
58+
}
15559

156-
if (isUtxoCoin(sdkCoin)) {
157-
return handleUtxoLikeRecovery(sdkCoin, req.enclavedExpressClient, {
158-
userKey: userPub,
159-
backupKey: backupPub,
160-
bitgoKey: bitgoPub,
161-
ignoreAddressTypes: coinSpecificParams?.ignoreAddressTypes ?? [],
162-
scan: coinSpecificParams?.addressScan,
163-
feeRate: coinSpecificParams?.feeRate,
164-
recoveryDestination: recoveryDestinationAddress,
165-
apiKey,
166-
});
60+
return { signedTxs };
61+
} catch (err) {
62+
logger.error('Error during consolidation recovery:', err);
63+
throw err;
16764
}
168-
169-
throw new MethodNotImplementedError('Recovery wallet is not supported for this coin: ' + coin);
17065
}

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) {
@@ -187,6 +188,26 @@ const RecoveryWalletRequest = {
187188

188189
export type RecoveryWalletRequest = typeof RecoveryWalletRequest;
189190

191+
const RecoveryConsolidationsWalletRequest = {
192+
userPub: t.string,
193+
backupPub: t.string,
194+
bitgoPub: t.union([t.undefined, t.string]),
195+
walletContractAddress: t.string,
196+
recoveryDestinationAddress: t.string,
197+
apiKey: t.string,
198+
durableNonces: t.union([t.undefined, t.boolean]),
199+
};
200+
201+
// Response type for /recoveryconsolidations endpoint
202+
const RecoveryConsolidationsWalletResponse: HttpResponse = {
203+
200: t.any,
204+
500: t.type({
205+
error: t.string,
206+
details: t.string,
207+
}),
208+
};
209+
210+
190211
export const ConsolidateUnspentsRequest = {
191212
pubkey: t.string,
192213
source: t.union([t.literal('user'), t.literal('backup')]),
@@ -263,6 +284,20 @@ export const MasterApiSpec = apiSpec({
263284
description: 'Recover an existing wallet',
264285
}),
265286
},
287+
'v1.wallet.recoveryConsolidations': {
288+
post: httpRoute({
289+
method: 'POST',
290+
path: '/api/{coin}/wallet/recoveryconsolidations',
291+
request: httpRequest({
292+
params: {
293+
coin: t.string,
294+
},
295+
body: RecoveryConsolidationsWalletRequest,
296+
}),
297+
response: RecoveryConsolidationsWalletResponse,
298+
description: 'Consolidate and recover an existing wallet',
299+
}),
300+
},
266301
'v1.wallet.consolidate': {
267302
post: httpRoute({
268303
method: 'POST',
@@ -368,6 +403,14 @@ export function createMasterApiRouter(
368403
}),
369404
]);
370405

406+
router.post('v1.wallet.recoveryConsolidations', [
407+
responseHandler<MasterExpressConfig>(async (req: express.Request) => {
408+
const typedReq = req as GenericMasterApiSpecRouteRequest;
409+
const result = await handleRecoveryConsolidationsOnPrem(typedReq);
410+
return Response.ok(result);
411+
}),
412+
]);
413+
371414
router.post('v1.wallet.accelerate', [
372415
responseHandler<MasterExpressConfig>(async (req: express.Request) => {
373416
const typedReq = req as GenericMasterApiSpecRouteRequest;

0 commit comments

Comments
 (0)