Skip to content

Commit 41f7b16

Browse files
feat(mbe): fix rebase
1 parent 944a945 commit 41f7b16

File tree

2 files changed

+89
-195
lines changed

2 files changed

+89
-195
lines changed
Lines changed: 46 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -1,214 +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 assert from 'assert';
7-
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';
18-
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
191
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
20-
21-
interface RecoveryParams {
22-
userKey: string;
23-
backupKey: string;
24-
walletContractAddress: string;
25-
recoveryDestination: string;
26-
apiKey: string;
27-
}
28-
29-
interface EnclavedRecoveryParams {
30-
userPub: string;
31-
backupPub: string;
32-
apiKey: string;
33-
unsignedSweepPrebuildTx: any; // TODO: type this properly once we have the SDK types
34-
coinSpecificParams?: Record<string, undefined>;
35-
walletContractAddress: string;
36-
}
37-
38-
async function handleEthLikeRecovery(
39-
sdkCoin: BaseCoin,
40-
commonRecoveryParams: RecoveryParams,
41-
enclavedExpressClient: any,
42-
params: EnclavedRecoveryParams,
43-
env: EnvironmentName,
44-
) {
45-
try {
46-
const { gasLimit, gasPrice, maxFeePerGas, maxPriorityFeePerGas } = DEFAULT_MUSIG_ETH_GAS_PARAMS;
47-
const unsignedSweepPrebuildTx = await (sdkCoin as AbstractEthLikeNewCoins).recover({
48-
...commonRecoveryParams,
49-
gasPrice,
50-
gasLimit,
51-
eip1559: {
52-
maxFeePerGas,
53-
maxPriorityFeePerGas,
54-
},
55-
replayProtectionOptions: getReplayProtectionOptions(env),
56-
});
57-
58-
const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({
59-
...params,
60-
unsignedSweepPrebuildTx,
61-
});
62-
63-
return fullSignedRecoveryTx;
64-
} catch (err) {
65-
throw err;
66-
}
67-
}
68-
69-
export type UtxoCoinSpecificRecoveryParams = Pick<
70-
Parameters<AbstractUtxoCoin['recover']>[0],
71-
| 'apiKey'
72-
| 'userKey'
73-
| 'backupKey'
74-
| 'bitgoKey'
75-
| 'ignoreAddressTypes'
76-
| 'scan'
77-
| 'feeRate'
78-
| 'recoveryDestination'
79-
>;
80-
81-
async function handleUtxoLikeRecovery(
82-
sdkCoin: BaseCoin,
83-
enclavedClient: EnclavedExpressClient,
84-
recoveryParams: UtxoCoinSpecificRecoveryParams,
85-
): Promise<{ txHex: string }> {
86-
const abstractUtxoCoin = sdkCoin as unknown as AbstractUtxoCoin;
87-
const recoverTx = await abstractUtxoCoin.recover(recoveryParams);
88-
89-
logger.info('UTXO recovery transaction created:', recoverTx);
90-
if (!isFormattedOfflineVaultTxInfo(recoverTx)) {
91-
throw new MethodNotImplementedError(`Unknown transaction ${JSON.stringify(recoverTx)} created`);
92-
}
93-
94-
return (await enclavedClient.recoveryMultisig({
95-
userPub: recoveryParams.userKey,
96-
backupPub: recoveryParams.backupKey,
97-
bitgoPub: recoveryParams.bitgoKey,
98-
unsignedSweepPrebuildTx: recoverTx,
99-
walletContractAddress: '',
100-
})) as { txHex: string };
101-
}
102-
103-
export async function handleRecoveryWalletOnPrem(
104-
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'>,
10510
) {
10611
const bitgo = req.bitgo;
10712
const coin = req.decoded.coin;
10813
const enclavedExpressClient = req.enclavedExpressClient;
109-
const { recoveryDestinationAddress, coinSpecificParams } = req.decoded;
110-
111-
const sdkCoin = bitgo.coin(coin);
11214

113-
// Handle TSS recovery
114-
if (req.decoded.isTssRecovery) {
115-
assert(req.decoded.tssRecoveryParams, 'TSS recovery parameters are required');
116-
const { commonKeychain } = req.decoded.tssRecoveryParams;
117-
if (!commonKeychain) {
118-
throw new Error('Common keychain is required for TSS recovery');
119-
}
15+
const { userPub, backupPub, bitgoPub } = req.decoded;
12016

121-
if (isEddsaCoin(sdkCoin)) {
122-
return handleEddsaRecovery(
123-
req.bitgo,
124-
sdkCoin,
125-
{
126-
userKey: commonKeychain,
127-
backupKey: commonKeychain,
128-
walletContractAddress: '',
129-
recoveryDestination: recoveryDestinationAddress,
130-
apiKey: req.decoded.apiKey || '',
131-
},
132-
enclavedExpressClient,
133-
{
134-
userPub: commonKeychain,
135-
backupPub: commonKeychain,
136-
apiKey: '',
137-
walletContractAddress: '',
138-
unsignedSweepPrebuildTx: undefined,
139-
coinSpecificParams: undefined,
140-
},
141-
);
142-
} else {
143-
throw new MethodNotImplementedError(
144-
`TSS recovery is not implemented for coin: ${coin}. Supported coins are Eddsa coins.`,
145-
);
146-
}
147-
}
148-
149-
// Handle standard recovery
150-
if (!req.decoded.multiSigRecoveryParams) {
151-
throw new Error('MultiSig recovery parameters are required for standard recovery');
152-
}
153-
154-
const { userPub, backupPub, bitgoPub, walletContractAddress } =
155-
req.decoded.multiSigRecoveryParams;
156-
const apiKey = req.decoded.apiKey || '';
157-
158-
if (!userPub || !backupPub) {
159-
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');
16022
}
16123

162-
// Check if the public key is valid
163-
if (!sdkCoin.isValidPub(userPub)) {
164-
throw new Error('Invalid user public key format');
165-
} else if (!sdkCoin.isValidPub(backupPub)) {
166-
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}`);
16726
}
16827

169-
const commonRecoveryParams: RecoveryParams = {
28+
// Use type assertion to access recoverConsolidations
29+
const result = await (sdkCoin as any).recoverConsolidations({
30+
...req.decoded,
17031
userKey: userPub,
17132
backupKey: backupPub,
172-
walletContractAddress,
173-
recoveryDestination: recoveryDestinationAddress,
174-
apiKey,
175-
};
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+
}
17644

177-
if (isEthLikeCoin(sdkCoin)) {
178-
if (!walletContractAddress) {
179-
throw new Error('Missing walletContract address');
180-
}
181-
return handleEthLikeRecovery(
182-
sdkCoin,
183-
commonRecoveryParams,
184-
enclavedExpressClient,
185-
{
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({
18652
userPub,
18753
backupPub,
188-
apiKey,
189-
unsignedSweepPrebuildTx: undefined,
190-
coinSpecificParams: undefined,
191-
walletContractAddress,
192-
},
193-
bitgo.env as EnvironmentName,
194-
);
195-
}
196-
if (!bitgoPub) {
197-
throw new Error('BitGo public key is required for recovery');
198-
}
54+
unsignedSweepPrebuildTx: tx,
55+
walletContractAddress: '',
56+
});
57+
signedTxs.push(signedTx);
58+
}
19959

200-
if (isUtxoCoin(sdkCoin)) {
201-
return handleUtxoLikeRecovery(sdkCoin, req.enclavedExpressClient, {
202-
userKey: userPub,
203-
backupKey: backupPub,
204-
bitgoKey: bitgoPub,
205-
ignoreAddressTypes: coinSpecificParams?.ignoreAddressTypes ?? [],
206-
scan: coinSpecificParams?.addressScan,
207-
feeRate: coinSpecificParams?.feeRate,
208-
recoveryDestination: recoveryDestinationAddress,
209-
apiKey,
210-
});
60+
return { signedTxs };
61+
} catch (err) {
62+
logger.error('Error during consolidation recovery:', err);
63+
throw err;
21164
}
212-
213-
throw new MethodNotImplementedError('Recovery wallet is not supported for this coin: ' + coin);
21465
}

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)