Skip to content

Commit 9ce5813

Browse files
feat(mbe): fix rebase
1 parent 41f7b16 commit 9ce5813

File tree

8 files changed

+557
-92
lines changed

8 files changed

+557
-92
lines changed

src/__tests__/api/master/musigRecovery.test.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('POST /api/:coin/wallet/recovery', () => {
7575
userPub: ethRecoveryData.userKey,
7676
backupPub: ethRecoveryData.backupKey,
7777
walletContractAddress: ethRecoveryData.walletContractAddress,
78-
bitgoPub: undefined,
78+
bitgoPub: '',
7979
},
8080
apiKey: 'etherscan-api-token',
8181
recoveryDestinationAddress: ethRecoveryData.recoveryDestinationAddress,
@@ -158,18 +158,4 @@ describe('POST /api/:coin/wallet/recovery', () => {
158158
responseNoBackupKey.body.should.have.property('error');
159159
responseNoBackupKey.body.error.should.match(/backupPub/i);
160160
});
161-
162-
it('should fail TSS recovery when common keychain is missing', async () => {
163-
const response = await agent
164-
.post(`/api/${coin}/wallet/recovery`)
165-
.set('Authorization', `Bearer ${accessToken}`)
166-
.send({
167-
isTssRecovery: true,
168-
recoveryDestinationAddress: ethRecoveryData.recoveryDestinationAddress,
169-
});
170-
171-
response.status.should.equal(400);
172-
response.body.should.have.property('error');
173-
response.body.error.should.match(/TSS recovery parameters are required/i);
174-
});
175161
});
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { BitGoAPI } from '@bitgo/sdk-api';
2+
import {
3+
BaseCoin,
4+
BaseTransactionBuilder,
5+
Eddsa,
6+
EDDSAMethods,
7+
EDDSAMethodTypes,
8+
PublicKey,
9+
} from '@bitgo/sdk-core';
10+
import { Ed25519Bip32HdTree } from '@bitgo/sdk-lib-mpc';
11+
import { CoinFamily, coins } from '@bitgo/statics';
12+
import { type KeyPair as SolKeyPair } from '@bitgo/sdk-coin-sol';
13+
import { retrieveKmsPrvKey } from '../utils';
14+
import { EnclavedConfig } from '../../../shared/types';
15+
import logger from '../../../logger';
16+
17+
async function setupTransactionBuilder(
18+
sdk: BitGoAPI,
19+
coinFamily: CoinFamily,
20+
signableHex: string,
21+
accountId: string,
22+
): Promise<{ txBuilder: BaseTransactionBuilder; publicKey: string }> {
23+
let modules;
24+
switch (coinFamily) {
25+
case CoinFamily.NEAR:
26+
modules = await import('@bitgo/sdk-coin-near');
27+
break;
28+
case CoinFamily.DOT:
29+
modules = await import('@bitgo/sdk-coin-dot');
30+
break;
31+
case CoinFamily.SUI:
32+
modules = await import('@bitgo/sdk-coin-sui');
33+
break;
34+
case CoinFamily.ADA:
35+
modules = await import('@bitgo/sdk-coin-ada');
36+
break;
37+
case CoinFamily.SOL:
38+
modules = await import('@bitgo/sdk-coin-sol');
39+
break;
40+
default:
41+
throw new Error(`Unsupported coin family: ${coinFamily}`);
42+
}
43+
44+
const { TransactionBuilderFactory, register, KeyPair } = modules;
45+
register(sdk);
46+
47+
const staticCoin = coins.get(coinFamily);
48+
try {
49+
const keyPair = new KeyPair({ pub: accountId });
50+
51+
let txBuilder;
52+
let publicKey: string;
53+
if (coinFamily === CoinFamily.SOL) {
54+
txBuilder = new TransactionBuilderFactory(staticCoin).from(signableHex);
55+
// For Solana, we need to use the getAddress method to derive the public key
56+
publicKey = (keyPair as SolKeyPair).getAddress();
57+
} else {
58+
txBuilder = new TransactionBuilderFactory(staticCoin).from(
59+
Buffer.from(signableHex, 'hex').toString('base64'),
60+
);
61+
publicKey = keyPair.getKeys().pub;
62+
}
63+
64+
return { txBuilder, publicKey };
65+
} catch (error) {
66+
logger.error(error);
67+
throw error;
68+
}
69+
}
70+
71+
export type SignEddsaRecoveryTransactionParams = {
72+
sdk: BitGoAPI;
73+
request: {
74+
commonKeychain: string;
75+
signableHex: string;
76+
derivationPath: string;
77+
};
78+
cfg: EnclavedConfig;
79+
coin: BaseCoin;
80+
};
81+
82+
export async function signEddsaRecoveryTransaction({
83+
sdk,
84+
cfg,
85+
request,
86+
coin,
87+
}: SignEddsaRecoveryTransactionParams) {
88+
let publicKey = '';
89+
logger.info(`Received request ${JSON.stringify(request)}`);
90+
91+
const hdTree = await Ed25519Bip32HdTree.initialize();
92+
const MPC = await Eddsa.initialize(hdTree);
93+
94+
const accountId = MPC.deriveUnhardened(request.commonKeychain, request.derivationPath).slice(
95+
0,
96+
64,
97+
);
98+
99+
const { txBuilder, publicKey: derivedKey } = await setupTransactionBuilder(
100+
sdk,
101+
coin.getFamily() as CoinFamily,
102+
request.signableHex,
103+
accountId,
104+
);
105+
106+
publicKey = derivedKey;
107+
// Get user and backup private keys
108+
const userPrv = await retrieveKmsPrvKey({
109+
pub: request.commonKeychain.toString(),
110+
source: 'user',
111+
cfg,
112+
options: { useLocalEncipherment: false },
113+
});
114+
115+
const backupPrv = await retrieveKmsPrvKey({
116+
pub: request.commonKeychain.toString(),
117+
source: 'backup',
118+
cfg,
119+
options: { useLocalEncipherment: false },
120+
});
121+
122+
if (!userPrv || !backupPrv) {
123+
throw new Error('Missing required private keys for recovery');
124+
}
125+
126+
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
127+
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
128+
129+
try {
130+
const signatureHex = await EDDSAMethods.getTSSSignature(
131+
userSigningMaterial,
132+
backupSigningMaterial,
133+
request.derivationPath,
134+
await txBuilder.build(),
135+
);
136+
137+
const publicKeyObj = { pub: publicKey };
138+
txBuilder.addSignature(publicKeyObj as PublicKey, signatureHex);
139+
const signedTx = await txBuilder.build();
140+
const serializedTx = signedTx.toBroadcastFormat();
141+
142+
return {
143+
txHex: serializedTx,
144+
};
145+
} catch (error) {
146+
throw error;
147+
}
148+
}

src/api/master/clients/enclavedExpressClient.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
Keychain,
1515
ApiKeyShare,
1616
MPCTx,
17+
MPCSweepTxs,
18+
MPCTxs,
1719
} from '@bitgo/sdk-core';
1820
import { RecoveryTransaction } from '@bitgo/sdk-coin-trx';
1921
import { superagentRequestFactory, buildApiClient, ApiClient } from '@api-ts/superagent-wrapper';
@@ -32,6 +34,7 @@ import {
3234
MpcV2RoundResponseType,
3335
} from '../../../enclavedBitgoExpress/routers/enclavedApiSpec';
3436
import { FormattedOfflineVaultTxInfo } from '@bitgo/abstract-utxo';
37+
import { RecoveryTxRequest } from 'bitgo';
3538

3639
const debugLogger = debug('bitgo:express:enclavedExpressClient');
3740

@@ -171,7 +174,7 @@ export interface SignMpcV2Round3Response {
171174

172175
export class EnclavedExpressClient {
173176
async recoveryMPC(params: {
174-
unsignedSweepPrebuildTx: MPCTx | MPCSweepTxs | MPCTxs;
177+
unsignedSweepPrebuildTx: MPCTx | MPCSweepTxs | MPCTxs | RecoveryTxRequest;
175178
userPub: string;
176179
backupPub: string;
177180
apiKey: string;

src/api/master/handlers/recoveryConsolidationsWallet.ts

Lines changed: 102 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,67 @@
11
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
22
import logger from '../../../logger';
3-
import { isSolCoin } from '../../../shared/coinUtils';
4-
import { MPCTx } from 'bitgo';
3+
import { BaseCoin, MPCConsolidationRecoveryOptions, MPCTx, RecoveryTxRequest } from 'bitgo';
54
import { RecoveryTransaction } from '@bitgo/sdk-coin-trx';
5+
import { BitGoBase } from '@bitgo/sdk-core';
6+
import { CoinFamily } from '@bitgo/statics';
7+
import type { Sol, SolConsolidationRecoveryOptions, Tsol } from '@bitgo/sdk-coin-sol';
8+
import type { Trx, ConsolidationRecoveryOptions, Ttrx } from '@bitgo/sdk-coin-trx';
9+
import type { Sui, Tsui } from '@bitgo/sdk-coin-sui';
10+
import type { Ada, Tada } from '@bitgo/sdk-coin-ada';
11+
import type { Dot, Tdot } from '@bitgo/sdk-coin-dot';
12+
import type { Tao, Ttao } from '@bitgo/sdk-coin-tao';
13+
14+
type RecoveryConsolidationParams =
15+
| ConsolidationRecoveryOptions
16+
| SolConsolidationRecoveryOptions
17+
| MPCConsolidationRecoveryOptions;
18+
19+
type RecoveryConsolidationResult = {
20+
transactions?: (RecoveryTransaction | MPCTx)[];
21+
txRequests?: RecoveryTxRequest[];
22+
};
23+
24+
export async function recoveryConsolidateWallets(
25+
sdk: BitGoBase,
26+
baseCoin: BaseCoin,
27+
params: RecoveryConsolidationParams,
28+
): Promise<RecoveryConsolidationResult> {
29+
const family = baseCoin.getFamily();
30+
31+
switch (family) {
32+
case CoinFamily.SOL: {
33+
const { register } = await import('@bitgo/sdk-coin-sol');
34+
register(sdk);
35+
const solCoin = baseCoin as unknown as Sol | Tsol;
36+
return await solCoin.recoverConsolidations(params as SolConsolidationRecoveryOptions);
37+
}
38+
case CoinFamily.TRX: {
39+
const { register } = await import('@bitgo/sdk-coin-trx');
40+
register(sdk);
41+
const trxCoin = baseCoin as unknown as Trx | Ttrx;
42+
return await trxCoin.recoverConsolidations(params as ConsolidationRecoveryOptions);
43+
}
44+
default: {
45+
const [
46+
{ register: registerSui },
47+
{ register: registerAda },
48+
{ register: registerDot },
49+
{ register: registerTao },
50+
] = await Promise.all([
51+
import('@bitgo/sdk-coin-sui'),
52+
import('@bitgo/sdk-coin-ada'),
53+
import('@bitgo/sdk-coin-dot'),
54+
import('@bitgo/sdk-coin-tao'),
55+
]);
56+
registerAda(sdk);
57+
registerSui(sdk);
58+
registerDot(sdk);
59+
registerTao(sdk);
60+
const coin = baseCoin as unknown as Sui | Tsui | Ada | Tada | Dot | Tdot | Tao | Ttao;
61+
return await coin.recoverConsolidations(params as MPCConsolidationRecoveryOptions);
62+
}
63+
}
64+
}
665

766
// Handler for recovery from receive addresses (consolidation sweeps)
867
export async function handleRecoveryConsolidationsOnPrem(
@@ -12,48 +71,67 @@ export async function handleRecoveryConsolidationsOnPrem(
1271
const coin = req.decoded.coin;
1372
const enclavedExpressClient = req.enclavedExpressClient;
1473

15-
const { userPub, backupPub, bitgoKey } = req.decoded;
74+
const isMPC = true;
1675

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');
76+
const { commonKeychain, apiKey } = req.decoded;
77+
let { userPub, backupPub, bitgoPub } = req.decoded;
78+
79+
if (isMPC) {
80+
if (!commonKeychain) {
81+
throw new Error('Missing required key: commonKeychain');
82+
}
83+
84+
userPub = commonKeychain;
85+
backupPub = commonKeychain;
86+
bitgoPub = commonKeychain;
2287
}
2388

24-
if (typeof (sdkCoin as any).recoverConsolidations !== 'function') {
25-
throw new Error(`recoverConsolidations is not supported for coin: ${coin}`);
89+
if (!userPub || !backupPub || !bitgoPub) {
90+
throw new Error('Missing required keys: userPub, backupPub, bitgoPub');
2691
}
2792

93+
const sdkCoin = bitgo.coin(coin);
94+
let txs: (RecoveryTransaction | MPCTx | RecoveryTxRequest)[] = [];
95+
2896
// Use type assertion to access recoverConsolidations
29-
const result = await (sdkCoin as any).recoverConsolidations({
97+
const result = await recoveryConsolidateWallets(bitgo, sdkCoin, {
3098
...req.decoded,
31-
userKey: userPub,
32-
backupKey: backupPub,
33-
bitgoKey,
34-
durableNonces: req.decoded.durableNonces,
99+
userKey: !isMPC ? userPub : '',
100+
backupKey: !isMPC ? backupPub : '',
101+
bitgoKey: bitgoPub,
35102
});
36103

37-
if ('transactions' in result) {
104+
console.log(`Recovery consolidations result: ${JSON.stringify(result)}`);
105+
106+
if (result.transactions) {
38107
txs = result.transactions;
39-
} else if ('txRequests' in result) {
108+
} else if (result.txRequests) {
40109
txs = result.txRequests;
41110
} else {
42111
throw new Error('recoverConsolidations did not return expected transactions');
43112
}
44113

45114
logger.debug(`Found ${txs.length} unsigned consolidation transactions`);
46115

47-
// 2. For each unsigned sweep, get it signed by EBE (using recoveryMultisig)
48116
const signedTxs = [];
49117
try {
50118
for (const tx of txs) {
51-
const signedTx = await enclavedExpressClient.recoveryMultisig({
52-
userPub,
53-
backupPub,
54-
unsignedSweepPrebuildTx: tx,
55-
walletContractAddress: '',
56-
});
119+
const signedTx = isMPC
120+
? await enclavedExpressClient.recoveryMPC({
121+
userPub,
122+
backupPub,
123+
apiKey,
124+
unsignedSweepPrebuildTx: tx as MPCTx | RecoveryTxRequest,
125+
coinSpecificParams: {},
126+
walletContractAddress: '',
127+
})
128+
: await enclavedExpressClient.recoveryMultisig({
129+
userPub,
130+
backupPub,
131+
unsignedSweepPrebuildTx: tx as RecoveryTransaction,
132+
walletContractAddress: '',
133+
});
134+
57135
signedTxs.push(signedTx);
58136
}
59137

0 commit comments

Comments
 (0)