Skip to content

Commit 693c2a8

Browse files
feat: typed mbe recovery endpoint req params
1 parent 478d5e1 commit 693c2a8

File tree

6 files changed

+99
-189
lines changed

6 files changed

+99
-189
lines changed
Lines changed: 10 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
11
import { SignFinalOptions } from '@bitgo/abstract-eth';
2-
import { MethodNotImplementedError } from 'bitgo';
32
import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec';
4-
import { KmsClient } from '../../kms/kmsClient';
53
import logger from '../../logger';
6-
import { isEosCoin, isEthCoin, isStxCoin, isUtxoCoin, isXtzCoin } from '../../shared/coinUtils';
4+
import { isEthCoin } from '../../shared/coinUtils';
5+
import { retrieveKmsKey } from './utils';
76

87
export async function recoveryMultisigTransaction(
98
req: EnclavedApiSpecRouteRequest<'v1.multisig.recovery', 'post'>,
109
): Promise<any> {
11-
const {
12-
userPub,
13-
backupPub,
14-
walletContractAddress,
15-
recoveryDestinationAddress,
16-
recoveryParams,
17-
apiKey,
18-
} = req.body;
10+
const { userPub, backupPub, unsignedSweepPrebuildTx, walletContractAddress } = req.body;
1911

2012
//fetch prv and check that pub are valid
2113
const userPrv = await retrieveKmsKey({ pub: userPub, source: 'user' });
22-
const backupPrv = await retrieveKmsKey({ pub: backupPub, source: 'user' });
14+
const backupPrv = await retrieveKmsKey({ pub: backupPub, source: 'backup' });
2315

2416
if (!userPrv || !backupPrv) {
2517
const errorMsg = `Error while recovery wallet, missing prv keys for user or backup on pub keys user=${userPub}, backup=${backupPub}`;
@@ -30,16 +22,6 @@ export async function recoveryMultisigTransaction(
3022
const bitgo = req.bitgo;
3123
const coin = bitgo.coin(req.params.coin);
3224

33-
//construct a common payload for the recovery that it's repeated in any kind of recovery
34-
const commonRecoveryParams = {
35-
userKey: userPub,
36-
backupKey: backupPub,
37-
walletContractAddress,
38-
recoveryDestination: recoveryDestinationAddress,
39-
// TODO: api key is not used so far because of a missconfig error on the bitgo obj
40-
apiKey,
41-
};
42-
4325
// The signed transaction format depends on the coin type so we do this check as a guard
4426
// If you check the type of coin before and after the "if", you may see "BaseCoin" vs "AbstractEthLikeCoin"
4527
if (coin.isEVM()) {
@@ -48,16 +30,11 @@ export async function recoveryMultisigTransaction(
4830
// TODO: populate coinSpecificParams with things like replayProtectionOptions
4931
// coinSpecificParams type could be "recoverOptions"
5032
try {
51-
const unsignedTx = await coin.recover({
52-
...commonRecoveryParams,
53-
//TODO: it's needed for keycard debugging, the walletPassphrase
54-
//walletPassphrase: passphrase,
55-
});
56-
5733
const halfSignedTx = await coin.signTransaction({
5834
isLastSignature: false,
5935
prv: userPrv,
60-
txPrebuild: { ...unsignedTx } as unknown as SignFinalOptions,
36+
txPrebuild: { ...unsignedSweepPrebuildTx } as unknown as SignFinalOptions,
37+
walletContractAddress,
6138
});
6239

6340
const { halfSigned } = halfSignedTx as any;
@@ -68,7 +45,9 @@ export async function recoveryMultisigTransaction(
6845
...halfSignedTx,
6946
txHex: halfSigned.signatures,
7047
halfSigned,
71-
},
48+
recipients: halfSigned.recipients ?? [],
49+
} as unknown as SignFinalOptions,
50+
walletContractAddress,
7251
signingKeyNonce: halfSigned.signingKeyNonce ?? 0,
7352
backupKeyNonce: halfSigned.backupKeyNonce ?? 0,
7453
recipients: halfSigned.recipients ?? [],
@@ -85,135 +64,6 @@ export async function recoveryMultisigTransaction(
8564
throw new Error(errorMsg);
8665
}
8766
} else {
88-
// TODO: from now on, this part isn't tested as we're lacking funds/apiKeys/etc
89-
// TODO: WIP
90-
// TODO (can't advance): XTZ throws a method not implemented on recover.
91-
if (isXtzCoin(coin)) {
92-
try {
93-
const unsignedTx = await coin.recover({
94-
...commonRecoveryParams,
95-
});
96-
97-
//TODO: fill this fields, check output from recover when recover implemented on sdk for xtz
98-
const txHex = '';
99-
const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined;
100-
const addressInfo = 'addressInfo' in unsignedTx ? unsignedTx.addressInfo : undefined;
101-
const feeInfo = 'feeInfo' in unsignedTx ? unsignedTx.feeInfo : undefined;
102-
const source = '';
103-
const dataToSign = '';
104-
105-
const halfSignedTx = await coin.signTransaction({
106-
txPrebuild: {
107-
txHex,
108-
txInfo,
109-
addressInfo,
110-
feeInfo,
111-
source,
112-
dataToSign,
113-
},
114-
prv: userPrv,
115-
});
116-
//TODO: continue with full sign and return that
117-
// still needs to be tested in order to deduce min payload
118-
return halfSignedTx;
119-
} catch (err) {
120-
console.log(err);
121-
throw err;
122-
}
123-
} else if (isStxCoin(coin)) {
124-
//TODO: (implementation untested): prioritize eth and btc instead of stc, when the other couple finished, go back to STX
125-
try {
126-
const unsignedTx = await coin.recover({
127-
...commonRecoveryParams,
128-
rootAddress: walletContractAddress, // TODO: is a root address the same as wallet contract address? where does root address comes from if not?
129-
});
130-
//TODO: continue with half sign and return that
131-
return unsignedTx;
132-
} catch (err) {
133-
console.log(err);
134-
throw err;
135-
}
136-
} else if (isEosCoin(coin)) {
137-
// TODO (implementation untested): we need some funds but faucets not working
138-
try {
139-
const unsignedTx = await coin.recover({
140-
...commonRecoveryParams,
141-
});
142-
143-
//TODO: continue with half sign and return that
144-
return unsignedTx;
145-
} catch (err) {
146-
console.log(err);
147-
throw err;
148-
}
149-
} else if (isUtxoCoin(coin)) {
150-
//TODO (implementation untested): we need an API key to complete/test btc flow
151-
//TODO: do we need a special case for BTC or is another UTXO-based coin?
152-
153-
const { bitgoPub } = recoveryParams;
154-
if (!bitgoPub) {
155-
logger.error('Missing bitgoPub in recoveryParams for UTXO coin recovery');
156-
throw new Error('Missing bitgoPub in recoveryParams for UTXO coin recovery');
157-
}
158-
try {
159-
const unsignedTx = await coin.recover({
160-
...commonRecoveryParams,
161-
bitgoKey: bitgoPub,
162-
ignoreAddressTypes: recoveryParams.ignoreAddressTypes || [],
163-
});
164-
165-
// some guards as the types have some imcompatibilities issues
166-
const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined;
167-
const txHex = 'txHex' in unsignedTx ? unsignedTx.txHex : '';
168-
169-
const halfSignedTx = await coin.signTransaction({
170-
txPrebuild: {
171-
txHex,
172-
txInfo,
173-
},
174-
prv: userPrv,
175-
});
176-
177-
const fullSignedTx = await coin.signTransaction({
178-
//TODO: check the body of this based on halfSignedTx output
179-
isLastSignature: true,
180-
txPrebuild: {
181-
txHex,
182-
txInfo,
183-
},
184-
signingStep: 'cosignerNonce',
185-
});
186-
187-
console.log(halfSignedTx);
188-
throw new MethodNotImplementedError(
189-
'Full signing for UTXO coins is not implemented in recovery yet. Please implement it.',
190-
);
191-
192-
return fullSignedTx;
193-
} catch (err) {
194-
console.log(err);
195-
throw err;
196-
}
197-
} else {
198-
throw new Error('Unsupported coin type for recovery: ' + coin);
199-
}
200-
}
201-
}
202-
203-
// TODO: this function is duplicated in multisigTransactioSign.ts but as hardcoded.
204-
// move both to an utils file
205-
async function retrieveKmsKey({ pub, source }: { pub: string; source: string }): Promise<string> {
206-
const kms = new KmsClient();
207-
// Retrieve the private key from KMS
208-
let prv: string;
209-
try {
210-
const res = await kms.getKey({ pub, source });
211-
prv = res.prv;
212-
return prv;
213-
} catch (error: any) {
214-
throw {
215-
status: error.status || 500,
216-
message: error.message || 'Failed to retrieve key from KMS',
217-
};
67+
throw new Error('Unsupported coin type for recovery: ' + coin);
21868
}
21969
}

src/api/enclaved/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// TODO: this function is duplicated in multisigTransactioSign.ts but as hardcoded. Replace that code later with this call (to avoid merge conflicts/duplication)
2+
import { KmsClient } from '../../kms/kmsClient';
3+
export async function retrieveKmsKey({
4+
pub,
5+
source,
6+
}: {
7+
pub: string;
8+
source: string;
9+
}): Promise<string> {
10+
const kms = new KmsClient();
11+
// Retrieve the private key from KMS
12+
let prv: string;
13+
try {
14+
const res = await kms.getKey({ pub, source });
15+
prv = res.prv;
16+
return prv;
17+
} catch (error: any) {
18+
throw {
19+
status: error.status || 500,
20+
message: error.message || 'Failed to retrieve key from KMS',
21+
};
22+
}
23+
}

src/enclavedBitgoExpress/routers/enclavedApiSpec.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,16 @@ const SignMultisigResponse: HttpResponse = {
5757
const RecoveryMultisigRequest = {
5858
userPub: t.string,
5959
backupPub: t.string,
60-
walletContractAddress: t.string,
61-
recoveryDestinationAddress: t.string,
62-
recoveryParams: t.any, // TODO: add more precise typing
60+
apiKey: t.string,
61+
// TODO: best typing for this, they come from sdk TS types
62+
unsignedSweepPrebuildTx: t.any,
63+
coinSpecificParams: t.union([
64+
t.undefined,
65+
t.partial({
66+
bitgoPub: t.union([t.undefined, t.string]),
67+
ignoreAddressTypes: t.union([t.undefined, t.array(t.string)]),
68+
}),
69+
]),
6370
};
6471

6572
// Response type for /multisig/recovery endpoint

src/masterBitgoExpress/enclavedExpressClient.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { OfflineVaultTxInfo, RecoveryInfo, UnsignedSweepTxMPCv2 } from '@bitgo/sdk-coin-eth';
12
import { SignedTransaction, TransactionPrebuild } from '@bitgo/sdk-core';
23
import debug from 'debug';
34
import https from 'https';
@@ -31,13 +32,14 @@ interface SignMultisigOptions {
3132
interface RecoveryMultisigOptions {
3233
userPub: string;
3334
backupPub: string;
34-
walletContractAddress: string;
35-
recoveryDestinationAddress: string;
35+
unsignedSweepPrebuildTx: RecoveryInfo | OfflineVaultTxInfo | UnsignedSweepTxMPCv2;
3636
apiKey: string;
37-
recoveryParams?: {
37+
walletContractAddress: string;
38+
coinSpecificParams?: {
3839
bitgoPub?: string;
39-
ignoreAddressTypes: string[];
40+
ignoreAddressTypes?: string[];
4041
};
42+
// recoveryDestinationAddress: string;
4143
}
4244

4345
export class EnclavedExpressClient {
Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import assert from 'assert';
2+
import { isEthCoin } from '../shared/coinUtils';
23
import { isMasterExpressConfig } from '../types';
34
import { createEnclavedExpressClient } from './enclavedExpressClient';
45
import { MasterApiSpecRouteRequest } from './routers/masterApiSpec';
56

67
export async function handleRecoveryWalletOnPrem(
78
req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>,
89
) {
10+
const bitgo = req.bitgo;
911
const coin = req.params.coin;
1012
assert(
1113
isMasterExpressConfig(req.config),
@@ -23,23 +25,44 @@ export async function handleRecoveryWalletOnPrem(
2325
backupPub,
2426
walletContractAddress,
2527
recoveryDestinationAddress,
26-
recoveryParams,
28+
coinSpecificParams,
2729
apiKey,
2830
} = req.body;
2931

30-
try {
31-
const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({
32-
userPub,
33-
backupPub,
34-
walletContractAddress,
35-
recoveryDestinationAddress,
36-
apiKey,
37-
recoveryParams,
38-
});
32+
//construct a common payload for the recovery that it's repeated in any kind of recovery
33+
const commonRecoveryParams = {
34+
userKey: userPub,
35+
backupKey: backupPub,
36+
walletContractAddress,
37+
recoveryDestination: recoveryDestinationAddress,
38+
apiKey,
39+
};
40+
41+
const sdkCoin = bitgo.coin(coin);
42+
43+
if (isEthCoin(sdkCoin)) {
44+
try {
45+
const unsignedSweepPrebuildTx = await sdkCoin.recover({
46+
...commonRecoveryParams,
47+
//TODO: DELETE. it's needed for keycard debugging, the walletPassphrase
48+
//walletPassphrase: passphrase,
49+
});
50+
const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({
51+
userPub,
52+
backupPub,
53+
apiKey,
54+
unsignedSweepPrebuildTx,
55+
coinSpecificParams,
56+
walletContractAddress,
57+
// recoveryDestinationAddress,
58+
});
3959

40-
return fullSignedRecoveryTx;
41-
} catch (err) {
42-
//TODO: check other error handling for ref on mbe
43-
throw err;
60+
return fullSignedRecoveryTx;
61+
} catch (err) {
62+
//TODO: check other error handling for ref on mbe
63+
throw err;
64+
}
65+
} else {
66+
throw new Error('Recovery wallet is not supported for this coin: ' + coin);
4467
}
4568
}

src/masterBitgoExpress/routers/masterApiSpec.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ export const SendManyRequest = {
7979

8080
export const SendManyResponse: HttpResponse = {
8181
// TODO: Get type from public types repo / Wallet Platform
82-
8382
200: t.any,
8483
500: t.type({
8584
error: t.string,
@@ -97,14 +96,20 @@ const RecoveryWalletResponse: HttpResponse = {
9796
}),
9897
};
9998

100-
// Request type for /generate endpoint
99+
// Request type for /recovery endpoint
101100
const RecoveryWalletRequest = {
102-
// TODO: complete the type
103-
// label: t.string,
104-
// multisigType: t.union([t.undefined, t.literal('onchain'), t.literal('tss')]),
105-
// enterprise: t.string,
106-
// disableTransactionNotifications: t.union([t.undefined, t.boolean]),
107-
// isDistributedCustody: t.union([t.undefined, t.boolean]),
101+
userPub: t.string,
102+
backupPub: t.string,
103+
walletContractAddress: t.string,
104+
recoveryDestinationAddress: t.string,
105+
apiKey: t.string,
106+
coinSpecificParams: t.union([
107+
t.undefined,
108+
t.partial({
109+
bitgoPub: t.union([t.undefined, t.string]),
110+
ignoreAddressTypes: t.union([t.undefined, t.array(t.string)]),
111+
}),
112+
]),
108113
};
109114

110115
// API Specification

0 commit comments

Comments
 (0)