Skip to content

Commit acc36e2

Browse files
feat: added partial typing - eve calls from mbe
1 parent e13869c commit acc36e2

3 files changed

Lines changed: 305 additions & 170 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// TODO: based on the original signMultisigTransaction.ts file.
2+
// Added this one because things like verify transaction doesn't seems to be present during recovery (in my limited experience)
3+
// But I want to commit something that could be fused later on with the normal signing
4+
5+
import { SignFinalOptions } from '@bitgo/abstract-eth';
6+
import { MethodNotImplementedError } from 'bitgo';
7+
import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec';
8+
import { KmsClient } from '../../kms/kmsClient';
9+
import logger from '../../logger';
10+
import { isEosCoin, isEthCoin, isStxCoin, isUtxoCoin, isXtzCoin } from '../../shared/coinUtils';
11+
12+
export async function recoveryMultisigTransaction(
13+
req: EnclavedApiSpecRouteRequest<'v1.multisig.recovery', 'post'>,
14+
): Promise<any> {
15+
const {
16+
userPub,
17+
backupPub,
18+
walletContractAddress,
19+
recoveryDestinationAddress,
20+
recoveryParams,
21+
apiKey,
22+
} = req.body;
23+
24+
//fetch prv and check that pub are valid
25+
const userPrv = await retrieveKmsKey({ pub: userPub, source: 'user' });
26+
const backupPrv = await retrieveKmsKey({ pub: backupPub, source: 'user' });
27+
28+
if (!userPrv || !backupPrv) {
29+
const errorMsg = `Error while recovery wallet, missing prv keys for user or backup on pub keys user=${userPub}, backup=${backupPub}`;
30+
logger.error(errorMsg);
31+
throw new Error(errorMsg);
32+
}
33+
34+
const bitgo = req.bitgo;
35+
const coin = bitgo.coin(req.params.coin);
36+
37+
//construct a common payload for the recovery that it's repeated in any kind of recovery
38+
const commonRecoveryParams = {
39+
userKey: userPub,
40+
backupKey: backupPub,
41+
walletContractAddress,
42+
recoveryDestination: recoveryDestinationAddress,
43+
// TODO: api key is not used so far because of a missconfig error on the bitgo obj
44+
apiKey,
45+
};
46+
47+
// The signed transaction format depends on the coin type so we do this check as a guard
48+
// If you check the type of coin before and after the "if", you may see "BaseCoin" vs "AbstractEthLikeCoin"
49+
if (coin.isEVM()) {
50+
// Every recovery method on every coin family varies one from another so we need to ensure with a guard.
51+
if (isEthCoin(coin)) {
52+
// TODO: populate coinSpecificParams with things like replayProtectionOptions
53+
// coinSpecificParams type could be "recoverOptions"
54+
try {
55+
const unsignedTx = await coin.recover({
56+
...commonRecoveryParams,
57+
//TODO: it's needed for keycard debugging, the walletPassphrase
58+
//walletPassphrase: passphrase,
59+
});
60+
61+
const halfSignedTx = await coin.signTransaction({
62+
isLastSignature: false,
63+
prv: userPrv,
64+
txPrebuild: { ...unsignedTx } as unknown as SignFinalOptions,
65+
});
66+
67+
const { halfSigned } = halfSignedTx as any;
68+
const fullSignedTx = await coin.signTransaction({
69+
isLastSignature: true,
70+
prv: backupPrv,
71+
txPrebuild: {
72+
...halfSignedTx,
73+
txHex: halfSigned.signatures,
74+
halfSigned,
75+
},
76+
signingKeyNonce: halfSigned.signingKeyNonce ?? 0,
77+
backupKeyNonce: halfSigned.backupKeyNonce ?? 0,
78+
recipients: halfSigned.recipients ?? [],
79+
});
80+
81+
return fullSignedTx;
82+
} catch (error) {
83+
logger.error('error while recovering wallet transaction:', error);
84+
throw error;
85+
}
86+
} else {
87+
const errorMsg = 'Unsupported coin type for recovery: ' + req.params.coin;
88+
logger.error(errorMsg);
89+
throw new Error(errorMsg);
90+
}
91+
} else {
92+
// TODO: from now on, this part isn't tested as we're lacking funds/apiKeys/etc
93+
// TODO: WIP
94+
// TODO (can't advance): XTZ throws a method not implemented on recover.
95+
if (isXtzCoin(coin)) {
96+
try {
97+
const unsignedTx = await coin.recover({
98+
...commonRecoveryParams,
99+
});
100+
101+
//TODO: fill this fields, check output from recover when recover implemented on sdk for xtz
102+
const txHex = '';
103+
const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined;
104+
const addressInfo = 'addressInfo' in unsignedTx ? unsignedTx.addressInfo : undefined;
105+
const feeInfo = 'feeInfo' in unsignedTx ? unsignedTx.feeInfo : undefined;
106+
const source = '';
107+
const dataToSign = '';
108+
109+
const halfSignedTx = await coin.signTransaction({
110+
txPrebuild: {
111+
txHex,
112+
txInfo,
113+
addressInfo,
114+
feeInfo,
115+
source,
116+
dataToSign,
117+
},
118+
prv: userPrv,
119+
});
120+
//TODO: continue with full sign and return that
121+
// still needs to be tested in order to deduce min payload
122+
return halfSignedTx;
123+
} catch (err) {
124+
console.log(err);
125+
throw err;
126+
}
127+
} else if (isStxCoin(coin)) {
128+
//TODO: (implementation untested): prioritize eth and btc instead of stc, when the other couple finished, go back to STX
129+
try {
130+
const unsignedTx = await coin.recover({
131+
...commonRecoveryParams,
132+
rootAddress: walletContractAddress, // TODO: is a root address the same as wallet contract address? where does root address comes from if not?
133+
});
134+
//TODO: continue with half sign and return that
135+
return unsignedTx;
136+
} catch (err) {
137+
console.log(err);
138+
throw err;
139+
}
140+
} else if (isEosCoin(coin)) {
141+
// TODO (implementation untested): we need some funds but faucets not working
142+
try {
143+
const unsignedTx = await coin.recover({
144+
...commonRecoveryParams,
145+
});
146+
147+
//TODO: continue with half sign and return that
148+
return unsignedTx;
149+
} catch (err) {
150+
console.log(err);
151+
throw err;
152+
}
153+
} else if (isUtxoCoin(coin)) {
154+
//TODO (implementation untested): we need an API key to complete/test btc flow
155+
//TODO: do we need a special case for BTC or is another UTXO-based coin?
156+
157+
const { bitgoPub } = recoveryParams;
158+
if (!bitgoPub) {
159+
logger.error('Missing bitgoPub in recoveryParams for UTXO coin recovery');
160+
throw new Error('Missing bitgoPub in recoveryParams for UTXO coin recovery');
161+
}
162+
try {
163+
const unsignedTx = await coin.recover({
164+
...commonRecoveryParams,
165+
bitgoKey: bitgoPub,
166+
ignoreAddressTypes: recoveryParams.ignoreAddressTypes || [],
167+
});
168+
169+
// some guards as the types have some imcompatibilities issues
170+
const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined;
171+
const txHex = 'txHex' in unsignedTx ? unsignedTx.txHex : '';
172+
173+
const halfSignedTx = await coin.signTransaction({
174+
txPrebuild: {
175+
txHex,
176+
txInfo,
177+
},
178+
prv: userPrv,
179+
});
180+
181+
const fullSignedTx = await coin.signTransaction({
182+
//TODO: check the body of this based on halfSignedTx output
183+
isLastSignature: true,
184+
txPrebuild: {
185+
txHex,
186+
txInfo,
187+
},
188+
signingStep: 'cosignerNonce',
189+
});
190+
191+
console.log(halfSignedTx);
192+
throw new MethodNotImplementedError(
193+
'Full signing for UTXO coins is not implemented in recovery yet. Please implement it.',
194+
);
195+
196+
return fullSignedTx;
197+
} catch (err) {
198+
console.log(err);
199+
throw err;
200+
}
201+
} else {
202+
throw new Error('Unsupported coin type for recovery: ' + coin);
203+
}
204+
}
205+
}
206+
207+
// TODO: this function is duplicated in multisigTransactioSign.ts but as hardcoded.
208+
// move both to an utils file
209+
async function retrieveKmsKey({ pub, source }: { pub: string; source: string }): Promise<string> {
210+
const kms = new KmsClient();
211+
// Retrieve the private key from KMS
212+
let prv: string;
213+
try {
214+
const res = await kms.getKey({ pub, source });
215+
prv = res.prv;
216+
return prv;
217+
} catch (error: any) {
218+
throw {
219+
status: error.status || 500,
220+
message: error.message || 'Failed to retrieve key from KMS',
221+
};
222+
}
223+
}

src/enclavedBitgoExpress/routers/enclavedApiSpec.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
import * as t from 'io-ts';
21
import {
32
apiSpec,
4-
httpRoute,
3+
Method as HttpMethod,
54
httpRequest,
65
HttpResponse,
7-
Method as HttpMethod,
6+
httpRoute,
87
} from '@api-ts/io-ts-http';
8+
import { Response } from '@api-ts/response';
99
import {
1010
createRouter,
11-
type WrappedRouter,
1211
TypedRequestHandler,
12+
type WrappedRouter,
1313
} from '@api-ts/typed-express-router';
14-
import { Response } from '@api-ts/response';
1514
import express from 'express';
16-
import { BitGoRequest } from '../../types/request';
17-
import { EnclavedConfig } from '../../types';
15+
import * as t from 'io-ts';
1816
import { postIndependentKey } from '../../api/enclaved/postIndependentKey';
17+
import { recoveryMultisigTransaction } from '../../api/enclaved/recoveryMultisigTransaction';
1918
import { signMultisigTransaction } from '../../api/enclaved/signMultisigTransaction';
2019
import { prepareBitGo, responseHandler } from '../../shared/middleware';
20+
import { EnclavedConfig } from '../../types';
21+
import { BitGoRequest } from '../../types/request';
2122

2223
// Request type for /key/independent endpoint
2324
const IndependentKeyRequest = {
@@ -39,7 +40,7 @@ const IndependentKeyResponse: HttpResponse = {
3940
const SignMultisigRequest = {
4041
source: t.string,
4142
pub: t.string,
42-
txPrebuild: t.any, // TransactionPrebuild type from BitGo
43+
txPrebuild: t.any,
4344
};
4445

4546
// Response type for /multisig/sign endpoint
@@ -52,6 +53,25 @@ const SignMultisigResponse: HttpResponse = {
5253
}),
5354
};
5455

56+
// Request type for /multisig/recovery endpoint
57+
const RecoveryMultisigRequest = {
58+
userPub: t.string,
59+
backupPub: t.string,
60+
walletContractAddress: t.string,
61+
recoveryDestinationAddress: t.string,
62+
recoveryParams: t.any, // TODO: add more precise typing
63+
};
64+
65+
// Response type for /multisig/recovery endpoint
66+
const RecoveryMultisigResponse: HttpResponse = {
67+
// TODO: Define proper response type for recovery multisig transaction
68+
200: t.any, // the full signed tx
69+
500: t.type({
70+
error: t.string,
71+
details: t.string,
72+
}),
73+
};
74+
5575
// API Specification
5676
export const EnclavedAPiSpec = apiSpec({
5777
'v1.multisig.sign': {
@@ -68,6 +88,20 @@ export const EnclavedAPiSpec = apiSpec({
6888
description: 'Sign a multisig transaction',
6989
}),
7090
},
91+
'v1.multisig.recovery': {
92+
post: httpRoute({
93+
method: 'POST',
94+
path: '/{coin}/multisig/recovery',
95+
request: httpRequest({
96+
params: {
97+
coin: t.string,
98+
},
99+
body: RecoveryMultisigRequest,
100+
}),
101+
response: RecoveryMultisigResponse,
102+
description: 'Recover a multisig transaction',
103+
}),
104+
},
71105
'v1.key.independent': {
72106
post: httpRoute({
73107
method: 'POST',
@@ -121,5 +155,13 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter<typeof
121155
}),
122156
]);
123157

158+
router.post('v1.multisig.recovery', [
159+
responseHandler<EnclavedConfig>(async (req) => {
160+
const typedReq = req as EnclavedApiSpecRouteRequest<'v1.multisig.recovery', 'post'>;
161+
const result = await recoveryMultisigTransaction(typedReq);
162+
return Response.ok(result);
163+
}),
164+
]);
165+
124166
return router;
125167
}

0 commit comments

Comments
 (0)