Skip to content

Commit 72a4d2a

Browse files
authored
Merge pull request #105 from BitGo/WP-5513/throw-valid-error
feat(mbe): throw valid error when server is down
2 parents d258bc2 + 3207ec9 commit 72a4d2a

3 files changed

Lines changed: 117 additions & 64 deletions

File tree

src/api/master/clients/advancedWalletManagerClient.ts

Lines changed: 72 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -178,51 +178,6 @@ export interface SignMpcV2Round3Response {
178178
}
179179

180180
export class AdvancedWalletManagerClient {
181-
async recoveryMPC(params: {
182-
unsignedSweepPrebuildTx: MPCTx | MPCSweepTxs | MPCTxs | RecoveryTxRequest;
183-
userPub: string;
184-
backupPub: string;
185-
apiKey: string;
186-
coinSpecificParams?: Record<string, unknown>;
187-
walletContractAddress: string;
188-
}): Promise<SignedTransaction> {
189-
if (!this.coin) {
190-
throw new Error('Coin must be specified to recover MPC');
191-
}
192-
193-
try {
194-
logger.info('Recovering MPC for coin: %s', this.coin);
195-
196-
// Extract the required information from the sweep tx using our utility function
197-
const tx = params.unsignedSweepPrebuildTx;
198-
const { signableHex, derivationPath } = extractTransactionRequestInfo(tx);
199-
200-
const txRequest = {
201-
unsignedTx: '',
202-
signableHex,
203-
derivationPath,
204-
};
205-
206-
let request = this.apiClient['v1.mpc.recovery'].post({
207-
coin: this.coin,
208-
commonKeychain: params.userPub,
209-
unsignedSweepPrebuildTx: {
210-
txRequests: [txRequest],
211-
},
212-
});
213-
214-
if (this.tlsMode === TlsMode.MTLS) {
215-
request = request.agent(this.createHttpsAgent());
216-
}
217-
218-
const response = await request.decodeExpecting(200);
219-
return response.body;
220-
} catch (error) {
221-
const err = error as Error;
222-
logger.error('Failed to recover MPC: %s', err.message);
223-
throw err;
224-
}
225-
}
226181
private readonly baseUrl: string;
227182
private readonly advancedWalletManagerCert: string;
228183
private readonly tlsKey?: string;
@@ -276,6 +231,52 @@ export class AdvancedWalletManagerClient {
276231
});
277232
}
278233

234+
async recoveryMPC(params: {
235+
unsignedSweepPrebuildTx: MPCTx | MPCSweepTxs | MPCTxs | RecoveryTxRequest;
236+
userPub: string;
237+
backupPub: string;
238+
apiKey: string;
239+
coinSpecificParams?: Record<string, unknown>;
240+
walletContractAddress: string;
241+
}): Promise<SignedTransaction> {
242+
if (!this.coin) {
243+
throw new Error('Coin must be specified to recover MPC');
244+
}
245+
246+
try {
247+
logger.info('Recovering MPC for coin: %s', this.coin);
248+
249+
// Extract the required information from the sweep tx using our utility function
250+
const tx = params.unsignedSweepPrebuildTx;
251+
const { signableHex, derivationPath } = extractTransactionRequestInfo(tx);
252+
253+
const txRequest = {
254+
unsignedTx: '',
255+
signableHex,
256+
derivationPath,
257+
};
258+
259+
let request = this.apiClient['v1.mpc.recovery'].post({
260+
coin: this.coin,
261+
commonKeychain: params.userPub,
262+
unsignedSweepPrebuildTx: {
263+
txRequests: [txRequest],
264+
},
265+
});
266+
267+
if (this.tlsMode === TlsMode.MTLS) {
268+
request = request.agent(this.createHttpsAgent());
269+
}
270+
271+
const response = await request.decodeExpecting(200);
272+
return response.body;
273+
} catch (error) {
274+
const err = error as Error;
275+
logger.error('Failed to recover MPC: %s', err.message);
276+
throw err;
277+
}
278+
}
279+
279280
/**
280281
* Create an independent multisig key for a given source and coin
281282
*/
@@ -303,7 +304,7 @@ export class AdvancedWalletManagerClient {
303304
} catch (error) {
304305
logger.error(
305306
'Failed to create independent keychain: %s',
306-
(error as DecodeError).decodedResponse.body,
307+
(error as DecodeError).decodedResponse?.body,
307308
);
308309
throw error;
309310
}
@@ -333,7 +334,7 @@ export class AdvancedWalletManagerClient {
333334

334335
return response.body;
335336
} catch (error) {
336-
logger.error('Failed to sign multisig: %s', (error as DecodeError).decodedResponse.body);
337+
logger.error('Failed to sign multisig: %s', (error as DecodeError).decodedResponse?.body);
337338
throw error;
338339
}
339340
}
@@ -358,7 +359,7 @@ export class AdvancedWalletManagerClient {
358359
} catch (error) {
359360
logger.error(
360361
'Failed to ping Advanced Wallet Manager: %s',
361-
(error as DecodeError).decodedResponse.body,
362+
(error as DecodeError).decodedResponse?.body,
362363
);
363364
throw error;
364365
}
@@ -383,7 +384,7 @@ export class AdvancedWalletManagerClient {
383384
} catch (error) {
384385
logger.error(
385386
'Failed to get version information: %s',
386-
(error as DecodeError).decodedResponse.body,
387+
(error as DecodeError).decodedResponse?.body,
387388
);
388389
throw error;
389390
}
@@ -408,7 +409,7 @@ export class AdvancedWalletManagerClient {
408409

409410
return res.body;
410411
} catch (error) {
411-
logger.error('Failed to recover multisig: %s', (error as DecodeError).decodedResponse.body);
412+
logger.error('Failed to recover multisig: %s', (error as DecodeError).decodedResponse?.body);
412413
throw error;
413414
}
414415
}
@@ -441,7 +442,7 @@ export class AdvancedWalletManagerClient {
441442
} catch (error) {
442443
logger.error(
443444
'Failed to initialize MPC key generation: %s',
444-
(error as DecodeError).decodedResponse.body,
445+
(error as DecodeError).decodedResponse?.body,
445446
);
446447
throw error;
447448
}
@@ -489,7 +490,7 @@ export class AdvancedWalletManagerClient {
489490
} catch (error) {
490491
logger.error(
491492
'Failed to finalize MPC key generation: %s',
492-
(error as DecodeError).decodedResponse.body,
493+
(error as DecodeError).decodedResponse?.body,
493494
);
494495
throw error;
495496
}
@@ -515,7 +516,7 @@ export class AdvancedWalletManagerClient {
515516
} catch (error) {
516517
logger.error(
517518
'Failed to sign mpc commitment: %s',
518-
(error as DecodeError).decodedResponse.body,
519+
(error as DecodeError).decodedResponse?.body,
519520
);
520521
throw error;
521522
}
@@ -539,7 +540,7 @@ export class AdvancedWalletManagerClient {
539540
const response = await request.decodeExpecting(200);
540541
return response.body;
541542
} catch (error) {
542-
logger.error('Failed to sign mpc r-share: %s', (error as DecodeError).decodedResponse.body);
543+
logger.error('Failed to sign mpc r-share: %s', (error as DecodeError).decodedResponse?.body);
543544
throw error;
544545
}
545546
}
@@ -562,7 +563,7 @@ export class AdvancedWalletManagerClient {
562563
const response = await request.decodeExpecting(200);
563564
return response.body;
564565
} catch (error) {
565-
logger.error('Failed to sign mpc g-share: %s', (error as DecodeError).decodedResponse.body);
566+
logger.error('Failed to sign mpc g-share: %s', (error as DecodeError).decodedResponse?.body);
566567
throw error;
567568
}
568569
}
@@ -593,7 +594,7 @@ export class AdvancedWalletManagerClient {
593594
} catch (error) {
594595
logger.error(
595596
'Failed to initialize MPCv2 key generation: %s',
596-
(error as DecodeError).decodedResponse.body,
597+
(error as DecodeError).decodedResponse?.body,
597598
);
598599
throw error;
599600
}
@@ -632,7 +633,7 @@ export class AdvancedWalletManagerClient {
632633
} catch (error) {
633634
logger.error(
634635
'Failed to execute MPCv2 round: %s',
635-
(error as DecodeError).decodedResponse.body,
636+
(error as DecodeError).decodedResponse?.body,
636637
);
637638
throw error;
638639
}
@@ -668,7 +669,7 @@ export class AdvancedWalletManagerClient {
668669
} catch (error) {
669670
logger.error(
670671
'Failed to finalize MPCv2 key generation: %s',
671-
(error as DecodeError).decodedResponse.body,
672+
(error as DecodeError).decodedResponse?.body,
672673
);
673674
throw error;
674675
}
@@ -701,7 +702,10 @@ export class AdvancedWalletManagerClient {
701702
const response = await request.decodeExpecting(200);
702703
return response.body;
703704
} catch (error) {
704-
logger.error('Failed to sign MPCv2 round 1: %s', (error as DecodeError).decodedResponse.body);
705+
logger.error(
706+
'Failed to sign MPCv2 round 1: %s',
707+
(error as DecodeError).decodedResponse?.body,
708+
);
705709
throw error;
706710
}
707711
}
@@ -733,7 +737,10 @@ export class AdvancedWalletManagerClient {
733737
const response = await request.decodeExpecting(200);
734738
return response.body;
735739
} catch (error) {
736-
logger.error('Failed to sign MPCv2 round 2: %s', (error as DecodeError).decodedResponse.body);
740+
logger.error(
741+
'Failed to sign MPCv2 round 2: %s',
742+
(error as DecodeError).decodedResponse?.body,
743+
);
737744
throw error;
738745
}
739746
}
@@ -765,7 +772,10 @@ export class AdvancedWalletManagerClient {
765772
const response = await request.decodeExpecting(200);
766773
return response.body;
767774
} catch (error) {
768-
logger.error('Failed to sign MPCv2 round 3: %s', (error as DecodeError).decodedResponse.body);
775+
logger.error(
776+
'Failed to sign MPCv2 round 3: %s',
777+
(error as DecodeError).decodedResponse?.body,
778+
);
769779
throw error;
770780
}
771781
}
@@ -794,7 +804,7 @@ export class AdvancedWalletManagerClient {
794804
} catch (error: any) {
795805
logger.error(
796806
'Failed to recover MPCv2 wallet: %s',
797-
(error as DecodeError).decodedResponse.body,
807+
(error as DecodeError).decodedResponse?.body,
798808
);
799809
throw error;
800810
}

src/kms/kmsClient.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export class KmsClient {
5454
// Handles http erros from KMS
5555
private errorHandler(error: superagent.ResponseError, errorLog: string) {
5656
logger.error(errorLog, error);
57+
58+
if (['ECONNREFUSED', 'ENOTFOUND', 'ECONNRESET', 'ETIMEDOUT'].includes((error as any).code)) {
59+
throw error;
60+
}
61+
5762
switch (error.status) {
5863
case 400:
5964
throw new BadRequestError(error.response?.body.message);
@@ -113,7 +118,6 @@ export class KmsClient {
113118
if (this.agent) req = req.agent(this.agent);
114119
kmsResponse = await req;
115120
} catch (error: any) {
116-
console.log('Error getting key from KMS:', error);
117121
this.errorHandler(error, 'Error getting key from KMS');
118122
}
119123

src/shared/responseHandler.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Request, Response as ExpressResponse, NextFunction } from 'express';
2-
import { Config } from '../shared/types';
2+
import { AppMode, Config } from '../shared/types';
33
import { BitGoRequest } from '../types/request';
44
import { ApiResponseError, AdvancedWalletManagerError } from '../errors';
55
import {
@@ -32,6 +32,30 @@ export type ServiceFunction<T extends Config = Config> = (
3232
next: NextFunction,
3333
) => Promise<ApiResponse> | ApiResponse;
3434

35+
/**
36+
* Check for specific API connection errors and throw appropriate messages
37+
*/
38+
function checkApiServerRunning(req: BitGoRequest, error: any): void {
39+
const config = req.config;
40+
const isMbe = config.appMode === AppMode.MASTER_EXPRESS;
41+
42+
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ECONNRESET') {
43+
throw new Error(
44+
`${
45+
isMbe ? 'Advanced Wallet Manager' : 'KMS'
46+
} API service is not running or unreachable. Please check if the service is available.`,
47+
);
48+
}
49+
50+
if (error.code === 'ETIMEDOUT' || error.timeout) {
51+
throw new Error(
52+
`${
53+
isMbe ? 'Advanced Wallet Manager' : 'KMS'
54+
} API request timed out. The service may be overloaded or unreachable.`,
55+
);
56+
}
57+
}
58+
3559
/**
3660
* Wraps a service function to handle Response objects and errors consistently
3761
* @param fn Service function that returns a Response object
@@ -43,6 +67,21 @@ export function responseHandler<T extends Config = Config>(fn: ServiceFunction<T
4367
const result = await fn(req as BitGoRequest<T>, res, next);
4468
return res.sendEncoded(result.type, result.payload);
4569
} catch (error) {
70+
// Check for API connection errors first, but don't throw if it's not a connection issue
71+
try {
72+
checkApiServerRunning(req as BitGoRequest, error);
73+
} catch (connectionError) {
74+
// If checkApiServerRunning throws, use that error message
75+
const errorBody = {
76+
error: 'Internal Server Error',
77+
name: 'ConnectionError',
78+
details:
79+
connectionError instanceof Error ? connectionError.message : String(connectionError),
80+
};
81+
logger.error('API Connection Error: %s', errorBody.details);
82+
return res.sendEncoded(500, errorBody);
83+
}
84+
4685
// If it's already a Response object (e.g. from Response.error)
4786
if (error && typeof error === 'object' && 'type' in error && 'payload' in error) {
4887
const apiError = error as ApiResponse;

0 commit comments

Comments
 (0)