Skip to content

Commit b6e6491

Browse files
committed
chore(mbe): handle bitgo api error responses
Ticket: WP-5260
1 parent fd128b6 commit b6e6491

File tree

7 files changed

+35
-68
lines changed

7 files changed

+35
-68
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ describe('POST /api/:coin/wallet/:walletId/accelerate', () => {
157157
cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
158158
});
159159

160-
response.status.should.equal(500);
160+
response.status.should.equal(404);
161161
response.body.should.have.property('error');
162162

163163
walletGetNock.done();
@@ -190,7 +190,7 @@ describe('POST /api/:coin/wallet/:walletId/accelerate', () => {
190190
cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
191191
});
192192

193-
response.status.should.equal(500);
193+
response.status.should.equal(404);
194194
response.body.should.have.property('error');
195195

196196
walletGetNock.done();

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

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -107,55 +107,6 @@ describe('POST /api/:coin/wallet/:walletId/consolidateunspents', () => {
107107
sinon.assert.calledOnce(consolidateUnspentsStub);
108108
});
109109

110-
it('should return error, name, and details on failure', async () => {
111-
const walletGetNock = nock(bitgoApiUrl)
112-
.get(`/api/v2/${coin}/wallet/${walletId}`)
113-
.matchHeader('any', () => true)
114-
.reply(200, {
115-
id: walletId,
116-
type: 'cold',
117-
subType: 'onPrem',
118-
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
119-
});
120-
121-
const keychainGetNock = nock(bitgoApiUrl)
122-
.get(`/api/v2/${coin}/key/user-key-id`)
123-
.matchHeader('any', () => true)
124-
.reply(200, {
125-
id: 'user-key-id',
126-
pub: 'xpub_user',
127-
});
128-
129-
const mockError = {
130-
error: 'Internal Server Error',
131-
name: 'ApiResponseError',
132-
details:
133-
'There are too few unspents that meet the given parameters to consolidate (1 available).',
134-
};
135-
136-
const consolidateUnspentsStub = sinon
137-
.stub(Wallet.prototype, 'consolidateUnspents')
138-
.throws(Object.assign(new Error(mockError.details), mockError));
139-
140-
const response = await agent
141-
.post(`/api/${coin}/wallet/${walletId}/consolidateunspents`)
142-
.set('Authorization', `Bearer ${accessToken}`)
143-
.send({
144-
source: 'user',
145-
pubkey: 'xpub_user',
146-
feeRate: 1000,
147-
});
148-
149-
response.status.should.equal(500);
150-
response.body.should.have.property('error', mockError.error);
151-
response.body.should.have.property('name', mockError.name);
152-
response.body.should.have.property('details', mockError.details);
153-
154-
walletGetNock.done();
155-
keychainGetNock.done();
156-
sinon.assert.calledOnce(consolidateUnspentsStub);
157-
});
158-
159110
it('should throw error when provided pubkey does not match wallet keychain', async () => {
160111
const walletGetNock = nock(bitgoApiUrl)
161112
.get(`/api/v2/${coin}/wallet/${walletId}`)

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
447447
pubkey: 'xpub_backup',
448448
});
449449

450-
response.status.should.equal(500);
450+
response.status.should.equal(400);
451451
response.body.details.should.equal('Backup MPC signing not supported for sendMany');
452452

453453
walletGetNock.done();
@@ -492,8 +492,7 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
492492
pubkey: 'wrong_pubkey',
493493
});
494494

495-
// TODO: Fix this to expect the error message when the middleware on MBE is fixed to handle errors
496-
response.status.should.equal(500);
495+
response.status.should.equal(400);
497496

498497
walletGetNock.done();
499498
keychainGetNock.done();
@@ -573,7 +572,7 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
573572
pubkey: 'xpub_user',
574573
});
575574

576-
response.status.should.equal(500);
575+
response.status.should.equal(400);
577576

578577
walletGetNock.done();
579578
keychainGetNock.done();
@@ -631,7 +630,7 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
631630
pubkey: 'xpub_user',
632631
});
633632

634-
response.status.should.equal(500);
633+
response.status.should.equal(400);
635634

636635
walletGetNock.done();
637636
keychainGetNock.done();

src/api/master/handlers/handleSendMany.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
1313
import { createEcdsaMPCv2CustomSigners } from './ecdsaMPCv2';
1414
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
1515
import { createEddsaCustomSigningFunctions } from './eddsa';
16+
import {BadRequestError, ForbiddenError, NotFoundError, UnprocessableEntityResponse} from '../../../shared/errors';
1617

1718
/**
1819
* Defines the structure for a single recipient in a send-many transaction.
@@ -41,7 +42,7 @@ function createMPCSendParamsWithCustomSigningFns(
4142
const mpcAlgorithm = coin.getMPCAlgorithm();
4243

4344
if (!commonKeychain) {
44-
throw new Error('Common keychain is required for MPC signing');
45+
throw new BadRequestError('Common keychain is required for MPC signing');
4546
}
4647

4748
if (mpcAlgorithm === 'ecdsa') {
@@ -66,10 +67,11 @@ function createMPCSendParamsWithCustomSigningFns(
6667
};
6768
}
6869

69-
throw new Error(`Unsupported MPC algorithm: ${mpcAlgorithm}`);
70+
throw new BadRequestError(`Unsupported MPC algorithm: ${mpcAlgorithm}`);
7071
}
7172

7273
export async function handleSendMany(req: MasterApiSpecRouteRequest<'v1.wallet.sendMany', 'post'>) {
74+
7375
const enclavedExpressClient = req.enclavedExpressClient;
7476
const reqId = new RequestTracer();
7577
const bitgo = req.bitgo;
@@ -81,11 +83,11 @@ export async function handleSendMany(req: MasterApiSpecRouteRequest<'v1.wallet.s
8183
const walletId = req.params.walletId;
8284
const wallet = await baseCoin.wallets().get({ id: walletId, reqId });
8385
if (!wallet) {
84-
throw new Error(`Wallet ${walletId} not found`);
86+
throw new NotFoundError(`Wallet ${walletId} not found`);
8587
}
8688

8789
if (wallet.type() !== 'cold' || wallet.subType() !== 'onPrem') {
88-
throw new Error('Wallet is not an on-prem wallet');
90+
throw new NotFoundError('Wallet is not an on-prem wallet');
8991
}
9092

9193
const keyIdIndex = params.source === 'user' ? KeyIndices.USER : KeyIndices.BACKUP;
@@ -98,13 +100,15 @@ export async function handleSendMany(req: MasterApiSpecRouteRequest<'v1.wallet.s
98100
});
99101

100102
if (!signingKeychain) {
101-
throw new Error(`Signing keychain for ${params.source} not found`);
103+
throw new NotFoundError(`Signing keychain for ${params.source} not found`);
102104
}
103105
if (params.pubkey && signingKeychain.pub !== params.pubkey) {
104-
throw new Error(`Pub provided does not match the keychain on wallet for ${params.source}`);
106+
throw new BadRequestError(
107+
`Pub provided does not match the keychain on wallet for ${params.source}`,
108+
);
105109
}
106110
if (params.commonKeychain && signingKeychain.commonKeychain !== params.commonKeychain) {
107-
throw new Error(
111+
throw new BadRequestError(
108112
`Common keychain provided does not match the keychain on wallet for ${params.source}`,
109113
);
110114
}
@@ -113,7 +117,7 @@ export async function handleSendMany(req: MasterApiSpecRouteRequest<'v1.wallet.s
113117
// Create MPC send parameters with custom signing functions
114118
if (wallet.multisigType() === 'tss') {
115119
if (signingKeychain.source === 'backup') {
116-
throw new Error('Backup MPC signing not supported for sendMany');
120+
throw new BadRequestError('Backup MPC signing not supported for sendMany');
117121
}
118122
const mpcSendParams = createMPCSendParamsWithCustomSigningFns(
119123
req,
@@ -150,14 +154,14 @@ export async function handleSendMany(req: MasterApiSpecRouteRequest<'v1.wallet.s
150154
walletType: wallet.multisigType(),
151155
});
152156
if (!verified) {
153-
throw new Error('Transaction prebuild failed local validation');
157+
throw new BadRequestError('Transaction prebuild failed local validation');
154158
}
155159
logger.debug('Transaction prebuild verified');
156160
} catch (e) {
157161
const err = e as Error;
158162
logger.error('transaction prebuild failed local validation:', err.message);
159163
logger.error('transaction prebuild:', JSON.stringify(txPrebuilt, null, 2));
160-
throw new Error(`Transaction prebuild failed local validation: ${err.message}`);
164+
throw new BadRequestError(`Transaction prebuild failed local validation: ${err.message}`);
161165
}
162166

163167
logger.debug('Tx prebuild: %s', JSON.stringify(txPrebuilt, null, 2));
@@ -188,7 +192,7 @@ export async function signAndSendMultisig(
188192
reqId: RequestTracer,
189193
) {
190194
if (!signingKeychain.pub) {
191-
throw new Error(`Signing keychain pub not found for ${source}`);
195+
throw new BadRequestError(`Signing keychain pub not found for ${source}`);
192196
}
193197
logger.info(`Signing with ${source} keychain, pub: ${signingKeychain.pub}`);
194198
logger.debug(`Signing keychain: ${JSON.stringify(signingKeychain, null, 2)}`);

src/api/master/routers/masterApiSpec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
BadRequestResponse,
3232
InternalServerErrorResponse,
3333
UnprocessableEntityResponse,
34+
NotFoundResponse,
3435
} from '../../../shared/errors';
3536

3637
export type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
@@ -158,6 +159,8 @@ export const SendManyRequest = {
158159
export const SendManyResponse: HttpResponse = {
159160
// TODO: Get type from public types repo / Wallet Platform
160161
200: t.any,
162+
...BadRequestResponse,
163+
...NotFoundResponse,
161164
...InternalServerErrorResponse,
162165
};
163166

src/shared/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,4 @@ const ErrorResponse = t.type({
8484
export const BadRequestResponse = { 400: ErrorResponse };
8585
export const UnprocessableEntityResponse = { 422: ErrorResponse };
8686
export const InternalServerErrorResponse = { 500: ErrorResponse };
87+
export const NotFoundResponse = { 404: ErrorResponse };

src/shared/responseHandler.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Request, Response as ExpressResponse, NextFunction } from 'express';
22
import { Config } from '../shared/types';
33
import { BitGoRequest } from '../types/request';
4-
import { EnclavedError } from '../errors';
4+
import { ApiResponseError, EnclavedError } from '../errors';
55
import {
66
BitgoExpressError,
77
ValidationError,
@@ -47,6 +47,15 @@ export function responseHandler<T extends Config = Config>(fn: ServiceFunction<T
4747
return res.sendEncoded(apiError.type, apiError.payload);
4848
}
4949

50+
if ((error as any).name === 'ApiResponseError') {
51+
const apiError = error as ApiResponseError;
52+
const body = {
53+
error: 'BitGoApiResponseError',
54+
details: apiError.result,
55+
};
56+
return res.status(apiError.status).json(body);
57+
}
58+
5059
// If it's a BitgoExpressError, map to appropriate status code
5160
if (error instanceof BitgoExpressError) {
5261
let statusCode = 500;

0 commit comments

Comments
 (0)