Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 72 additions & 62 deletions src/api/master/clients/advancedWalletManagerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,51 +178,6 @@ export interface SignMpcV2Round3Response {
}

export class AdvancedWalletManagerClient {
async recoveryMPC(params: {
unsignedSweepPrebuildTx: MPCTx | MPCSweepTxs | MPCTxs | RecoveryTxRequest;
userPub: string;
backupPub: string;
apiKey: string;
coinSpecificParams?: Record<string, unknown>;
walletContractAddress: string;
}): Promise<SignedTransaction> {
if (!this.coin) {
throw new Error('Coin must be specified to recover MPC');
}

try {
logger.info('Recovering MPC for coin: %s', this.coin);

// Extract the required information from the sweep tx using our utility function
const tx = params.unsignedSweepPrebuildTx;
const { signableHex, derivationPath } = extractTransactionRequestInfo(tx);

const txRequest = {
unsignedTx: '',
signableHex,
derivationPath,
};

let request = this.apiClient['v1.mpc.recovery'].post({
coin: this.coin,
commonKeychain: params.userPub,
unsignedSweepPrebuildTx: {
txRequests: [txRequest],
},
});

if (this.tlsMode === TlsMode.MTLS) {
request = request.agent(this.createHttpsAgent());
}

const response = await request.decodeExpecting(200);
return response.body;
} catch (error) {
const err = error as Error;
logger.error('Failed to recover MPC: %s', err.message);
throw err;
}
}
private readonly baseUrl: string;
private readonly advancedWalletManagerCert: string;
private readonly tlsKey?: string;
Expand Down Expand Up @@ -276,6 +231,52 @@ export class AdvancedWalletManagerClient {
});
}

async recoveryMPC(params: {
unsignedSweepPrebuildTx: MPCTx | MPCSweepTxs | MPCTxs | RecoveryTxRequest;
userPub: string;
backupPub: string;
apiKey: string;
coinSpecificParams?: Record<string, unknown>;
walletContractAddress: string;
}): Promise<SignedTransaction> {
if (!this.coin) {
throw new Error('Coin must be specified to recover MPC');
}

try {
logger.info('Recovering MPC for coin: %s', this.coin);

// Extract the required information from the sweep tx using our utility function
const tx = params.unsignedSweepPrebuildTx;
const { signableHex, derivationPath } = extractTransactionRequestInfo(tx);

const txRequest = {
unsignedTx: '',
signableHex,
derivationPath,
};

let request = this.apiClient['v1.mpc.recovery'].post({
coin: this.coin,
commonKeychain: params.userPub,
unsignedSweepPrebuildTx: {
txRequests: [txRequest],
},
});

if (this.tlsMode === TlsMode.MTLS) {
request = request.agent(this.createHttpsAgent());
}

const response = await request.decodeExpecting(200);
return response.body;
} catch (error) {
const err = error as Error;
logger.error('Failed to recover MPC: %s', err.message);
throw err;
}
}

/**
* Create an independent multisig key for a given source and coin
*/
Expand Down Expand Up @@ -303,7 +304,7 @@ export class AdvancedWalletManagerClient {
} catch (error) {
logger.error(
'Failed to create independent keychain: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does it need be optional now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested it and found that decodedResponse is undefined when the kms or aws is offline and it throws another error because of getting body from decodedResponse

);
throw error;
}
Expand Down Expand Up @@ -333,7 +334,7 @@ export class AdvancedWalletManagerClient {

return response.body;
} catch (error) {
logger.error('Failed to sign multisig: %s', (error as DecodeError).decodedResponse.body);
logger.error('Failed to sign multisig: %s', (error as DecodeError).decodedResponse?.body);
throw error;
}
}
Expand All @@ -358,7 +359,7 @@ export class AdvancedWalletManagerClient {
} catch (error) {
logger.error(
'Failed to ping Advanced Wallet Manager: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
Expand All @@ -383,7 +384,7 @@ export class AdvancedWalletManagerClient {
} catch (error) {
logger.error(
'Failed to get version information: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
Expand All @@ -408,7 +409,7 @@ export class AdvancedWalletManagerClient {

return res.body;
} catch (error) {
logger.error('Failed to recover multisig: %s', (error as DecodeError).decodedResponse.body);
logger.error('Failed to recover multisig: %s', (error as DecodeError).decodedResponse?.body);
throw error;
}
}
Expand Down Expand Up @@ -441,7 +442,7 @@ export class AdvancedWalletManagerClient {
} catch (error) {
logger.error(
'Failed to initialize MPC key generation: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
Expand Down Expand Up @@ -489,7 +490,7 @@ export class AdvancedWalletManagerClient {
} catch (error) {
logger.error(
'Failed to finalize MPC key generation: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
Expand All @@ -515,7 +516,7 @@ export class AdvancedWalletManagerClient {
} catch (error) {
logger.error(
'Failed to sign mpc commitment: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
Expand All @@ -539,7 +540,7 @@ export class AdvancedWalletManagerClient {
const response = await request.decodeExpecting(200);
return response.body;
} catch (error) {
logger.error('Failed to sign mpc r-share: %s', (error as DecodeError).decodedResponse.body);
logger.error('Failed to sign mpc r-share: %s', (error as DecodeError).decodedResponse?.body);
throw error;
}
}
Expand All @@ -562,7 +563,7 @@ export class AdvancedWalletManagerClient {
const response = await request.decodeExpecting(200);
return response.body;
} catch (error) {
logger.error('Failed to sign mpc g-share: %s', (error as DecodeError).decodedResponse.body);
logger.error('Failed to sign mpc g-share: %s', (error as DecodeError).decodedResponse?.body);
throw error;
}
}
Expand Down Expand Up @@ -593,7 +594,7 @@ export class AdvancedWalletManagerClient {
} catch (error) {
logger.error(
'Failed to initialize MPCv2 key generation: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
Expand Down Expand Up @@ -632,7 +633,7 @@ export class AdvancedWalletManagerClient {
} catch (error) {
logger.error(
'Failed to execute MPCv2 round: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
Expand Down Expand Up @@ -668,7 +669,7 @@ export class AdvancedWalletManagerClient {
} catch (error) {
logger.error(
'Failed to finalize MPCv2 key generation: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
Expand Down Expand Up @@ -701,7 +702,10 @@ export class AdvancedWalletManagerClient {
const response = await request.decodeExpecting(200);
return response.body;
} catch (error) {
logger.error('Failed to sign MPCv2 round 1: %s', (error as DecodeError).decodedResponse.body);
logger.error(
'Failed to sign MPCv2 round 1: %s',
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
}
Expand Down Expand Up @@ -733,7 +737,10 @@ export class AdvancedWalletManagerClient {
const response = await request.decodeExpecting(200);
return response.body;
} catch (error) {
logger.error('Failed to sign MPCv2 round 2: %s', (error as DecodeError).decodedResponse.body);
logger.error(
'Failed to sign MPCv2 round 2: %s',
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
}
Expand Down Expand Up @@ -765,7 +772,10 @@ export class AdvancedWalletManagerClient {
const response = await request.decodeExpecting(200);
return response.body;
} catch (error) {
logger.error('Failed to sign MPCv2 round 3: %s', (error as DecodeError).decodedResponse.body);
logger.error(
'Failed to sign MPCv2 round 3: %s',
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
}
Expand Down Expand Up @@ -794,7 +804,7 @@ export class AdvancedWalletManagerClient {
} catch (error: any) {
logger.error(
'Failed to recover MPCv2 wallet: %s',
(error as DecodeError).decodedResponse.body,
(error as DecodeError).decodedResponse?.body,
);
throw error;
}
Expand Down
6 changes: 5 additions & 1 deletion src/kms/kmsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export class KmsClient {
// Handles http erros from KMS
private errorHandler(error: superagent.ResponseError, errorLog: string) {
logger.error(errorLog, error);

if (['ECONNREFUSED', 'ENOTFOUND', 'ECONNRESET', 'ETIMEDOUT'].includes((error as any).code)) {
throw error;
}

switch (error.status) {
case 400:
throw new BadRequestError(error.response?.body.message);
Expand Down Expand Up @@ -113,7 +118,6 @@ export class KmsClient {
if (this.agent) req = req.agent(this.agent);
kmsResponse = await req;
} catch (error: any) {
console.log('Error getting key from KMS:', error);
this.errorHandler(error, 'Error getting key from KMS');
}

Expand Down
41 changes: 40 additions & 1 deletion src/shared/responseHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request, Response as ExpressResponse, NextFunction } from 'express';
import { Config } from '../shared/types';
import { AppMode, Config } from '../shared/types';
import { BitGoRequest } from '../types/request';
import { ApiResponseError, AdvancedWalletManagerError } from '../errors';
import {
Expand Down Expand Up @@ -32,6 +32,30 @@ export type ServiceFunction<T extends Config = Config> = (
next: NextFunction,
) => Promise<ApiResponse> | ApiResponse;

/**
* Check for specific API connection errors and throw appropriate messages
*/
function checkApiServerRunning(req: BitGoRequest, error: any): void {
const config = req.config;
const isMbe = config.appMode === AppMode.MASTER_EXPRESS;

if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ECONNRESET') {
throw new Error(
`${
isMbe ? 'Advanced Wallet Manager' : 'KMS'
} API service is not running or unreachable. Please check if the service is available.`,
);
}

if (error.code === 'ETIMEDOUT' || error.timeout) {
throw new Error(
`${
isMbe ? 'Advanced Wallet Manager' : 'KMS'
} API request timed out. The service may be overloaded or unreachable.`,
);
}
}

/**
* Wraps a service function to handle Response objects and errors consistently
* @param fn Service function that returns a Response object
Expand All @@ -43,6 +67,21 @@ export function responseHandler<T extends Config = Config>(fn: ServiceFunction<T
const result = await fn(req as BitGoRequest<T>, res, next);
return res.sendEncoded(result.type, result.payload);
} catch (error) {
// Check for API connection errors first, but don't throw if it's not a connection issue
try {
checkApiServerRunning(req as BitGoRequest, error);
} catch (connectionError) {
// If checkApiServerRunning throws, use that error message
const errorBody = {
error: 'Internal Server Error',
name: 'ConnectionError',
details:
connectionError instanceof Error ? connectionError.message : String(connectionError),
};
logger.error('API Connection Error: %s', errorBody.details);
return res.sendEncoded(500, errorBody);
}

// If it's already a Response object (e.g. from Response.error)
if (error && typeof error === 'object' && 'type' in error && 'payload' in error) {
const apiError = error as ApiResponse;
Expand Down