diff --git a/src/__tests__/api/enclaved/signMpcTransaction.test.ts b/src/__tests__/api/enclaved/signMpcTransaction.test.ts index 1d57b2d..561c6ce 100644 --- a/src/__tests__/api/enclaved/signMpcTransaction.test.ts +++ b/src/__tests__/api/enclaved/signMpcTransaction.test.ts @@ -269,7 +269,7 @@ describe('signMpcTransaction', () => { .set('Authorization', `Bearer ${accessToken}`) .send(input); - response.status.should.equal(500); + response.status.should.equal(404); response.body.should.have.property('error'); kmsNock.done(); }); diff --git a/src/__tests__/api/master/accelerate.test.ts b/src/__tests__/api/master/accelerate.test.ts index fdd80aa..129c11c 100644 --- a/src/__tests__/api/master/accelerate.test.ts +++ b/src/__tests__/api/master/accelerate.test.ts @@ -157,8 +157,10 @@ describe('POST /api/:coin/wallet/:walletId/accelerate', () => { cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'], }); - response.status.should.equal(500); + response.status.should.equal(404); response.body.should.have.property('error'); + response.body.error.should.equal('ApiResponseError'); + response.body.details.should.deepEqual({ error: 'Wallet not found' }); walletGetNock.done(); }); @@ -190,8 +192,10 @@ describe('POST /api/:coin/wallet/:walletId/accelerate', () => { cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'], }); - response.status.should.equal(500); + response.status.should.equal(404); response.body.should.have.property('error'); + response.body.error.should.equal('ApiResponseError'); + response.body.details.should.deepEqual({ error: 'Keychain not found' }); walletGetNock.done(); keychainGetNock.done(); diff --git a/src/__tests__/api/master/consolidate.test.ts b/src/__tests__/api/master/consolidate.test.ts index 352a926..eadf99e 100644 --- a/src/__tests__/api/master/consolidate.test.ts +++ b/src/__tests__/api/master/consolidate.test.ts @@ -145,11 +145,21 @@ describe('POST /api/:coin/wallet/:walletId/consolidate', () => { consolidateAddresses: ['0x1234567890abcdef', '0xfedcba0987654321'], }); - response.status.should.equal(500); - response.body.should.have.property('error', 'Internal Server Error'); - response.body.should.have - .property('details') - .which.match(/Consolidations failed: 1 and succeeded: 1/); + response.status.should.equal(202); + response.body.should.deepEqual({ + success: [ + { + txid: 'consolidation-tx-1', + status: 'signed', + }, + ], + failure: [ + { + error: 'Insufficient funds', + address: '0xfedcba0987654321', + }, + ], + }); walletGetNock.done(); keychainGetNock.done(); diff --git a/src/__tests__/api/master/consolidateUnspents.test.ts b/src/__tests__/api/master/consolidateUnspents.test.ts index 3665c53..c4c86ad 100644 --- a/src/__tests__/api/master/consolidateUnspents.test.ts +++ b/src/__tests__/api/master/consolidateUnspents.test.ts @@ -128,7 +128,6 @@ describe('POST /api/:coin/wallet/:walletId/consolidateunspents', () => { const mockError = { error: 'Internal Server Error', - name: 'ApiResponseError', details: 'There are too few unspents that meet the given parameters to consolidate (1 available).', }; @@ -147,9 +146,6 @@ describe('POST /api/:coin/wallet/:walletId/consolidateunspents', () => { }); response.status.should.equal(500); - response.body.should.have.property('error', mockError.error); - response.body.should.have.property('name', mockError.name); - response.body.should.have.property('details', mockError.details); walletGetNock.done(); keychainGetNock.done(); diff --git a/src/api/master/clients/enclavedExpressClient.ts b/src/api/master/clients/enclavedExpressClient.ts index 99fd15f..97c338a 100644 --- a/src/api/master/clients/enclavedExpressClient.ts +++ b/src/api/master/clients/enclavedExpressClient.ts @@ -30,6 +30,7 @@ import { MpcV2RoundResponseType, } from '../../../enclavedBitgoExpress/routers/enclavedApiSpec'; import { FormattedOfflineVaultTxInfo } from '@bitgo/abstract-utxo'; +import { EnclavedError } from '../../../errors'; const debugLogger = debug('bitgo:express:enclavedExpressClient'); @@ -242,7 +243,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to create independent keychain: %s', err.message); - throw err; + throw new EnclavedError(`Failed to create independent keychain: ${err.message}`, 500); } } @@ -272,7 +273,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to sign multisig: %s', err.message); - throw err; + throw new EnclavedError(`Failed to sign multisig transaction: ${err.message}`, 500); } } @@ -296,7 +297,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Enclaved express service ping failed: %s', err.message); - throw err; + throw new EnclavedError(`Failed to ping enclaved express service: ${err.message}`, 500); } } @@ -319,7 +320,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to get version information: %s', err.message); - throw err; + throw new EnclavedError(`Failed to get version information: ${err.message}`, 500); } } @@ -344,7 +345,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to recover multisig: %s', err.message); - throw err; + throw new EnclavedError(`Failed to recover multisig transaction: ${err.message}`, 500); } } @@ -376,7 +377,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to initialize MPC key generation: %s', err.message); - throw err; + throw new EnclavedError(`Failed to initialize MPC key generation: ${err.message}`, 500); } } @@ -422,7 +423,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to finalize MPC key generation: %s', err.message); - throw err; + throw new EnclavedError(`Failed to finalize MPC key generation: ${err.message}`, 500); } } @@ -446,7 +447,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to sign mpc commitment: %s', err.message); - throw err; + throw new EnclavedError(`Failed to sign MPC commitment: ${err.message}`, 500); } } @@ -470,7 +471,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to sign mpc r-share: %s', err.message); - throw err; + throw new EnclavedError(`Failed to sign MPC R-share: ${err.message}`, 500); } } @@ -494,7 +495,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to sign mpc g-share: %s', err.message); - throw err; + throw new EnclavedError(`Failed to sign MPC G-share: ${err.message}`, 500); } } @@ -524,7 +525,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to initialize MPCv2 key generation: %s', err.message); - throw err; + throw new EnclavedError(`Failed to initialize MPCv2 key generation: ${err.message}`, 500); } } @@ -561,7 +562,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to execute MPCv2 round: %s', err.message); - throw err; + throw new EnclavedError(`Failed to execute MPCv2 round: ${err.message}`, 500); } } @@ -595,7 +596,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to finalize MPCv2 key generation: %s', err.message); - throw err; + throw new EnclavedError(`Failed to finalize MPCv2 key generation: ${err.message}`, 500); } } @@ -628,7 +629,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to sign mpcv2 round 1: %s', err.message); - throw err; + throw new EnclavedError(`Failed to sign MPCv2 Round 1: ${err.message}`, 500); } } @@ -661,7 +662,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to sign mpcv2 round 2: %s', err.message); - throw err; + throw new EnclavedError(`Failed to sign MPCv2 Round 2: ${err.message}`, 500); } } @@ -694,7 +695,7 @@ export class EnclavedExpressClient { } catch (error) { const err = error as Error; debugLogger('Failed to sign mpcv2 round 3: %s', err.message); - throw err; + throw new EnclavedError(`Failed to sign MPCv2 Round 3: ${err.message}`, 500); } } } diff --git a/src/api/master/handlers/handleConsolidate.ts b/src/api/master/handlers/handleConsolidate.ts index 30940d4..53b7d94 100644 --- a/src/api/master/handlers/handleConsolidate.ts +++ b/src/api/master/handlers/handleConsolidate.ts @@ -61,7 +61,7 @@ export async function handleConsolidate( msg = `Consolidations failed: ${result.failure.length} and succeeded: ${result.success.length}`; } else { // All failed - status = 400; + status = 500; msg = 'All consolidations failed'; } diff --git a/src/api/master/routers/enclavedExpressHealth.ts b/src/api/master/routers/enclavedExpressHealth.ts index 31ab8ea..aea6219 100644 --- a/src/api/master/routers/enclavedExpressHealth.ts +++ b/src/api/master/routers/enclavedExpressHealth.ts @@ -14,6 +14,10 @@ const PingEnclavedResponse: HttpResponse = { status: t.string, enclavedResponse: PingResponseType, }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -22,6 +26,10 @@ const PingEnclavedResponse: HttpResponse = { const VersionEnclavedResponse: HttpResponse = { 200: VersionResponseType, + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, diff --git a/src/api/master/routers/healthCheck.ts b/src/api/master/routers/healthCheck.ts index 3706223..801dd2d 100644 --- a/src/api/master/routers/healthCheck.ts +++ b/src/api/master/routers/healthCheck.ts @@ -4,14 +4,23 @@ import { Response } from '@api-ts/response'; import pjson from '../../../../package.json'; import { responseHandler } from '../../../shared/middleware'; import { PingResponseType, VersionResponseType } from '../../../types/health'; +import * as t from 'io-ts'; // API Response types const PingResponse: HttpResponse = { 200: PingResponseType, + 404: t.type({ + error: t.string, + details: t.string, + }), }; const VersionResponse: HttpResponse = { 200: VersionResponseType, + 404: t.type({ + error: t.string, + details: t.string, + }), }; // API Specification diff --git a/src/api/master/routers/masterApiSpec.ts b/src/api/master/routers/masterApiSpec.ts index a9548e5..8d6f798 100644 --- a/src/api/master/routers/masterApiSpec.ts +++ b/src/api/master/routers/masterApiSpec.ts @@ -35,6 +35,14 @@ export function parseBody(req: express.Request, res: express.Response, next: exp const GenerateWalletResponse: HttpResponse = { // TODO: Get type from public types repo 200: t.any, + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -57,6 +65,7 @@ export const SendManyRequest = { t.undefined, t.literal('transfer'), t.literal('acceleration'), + t.literal('fillNonce'), t.literal('accountSet'), t.literal('enabletoken'), t.literal('stakingLock'), @@ -66,7 +75,7 @@ export const SendManyRequest = { ]), commonKeychain: t.union([t.undefined, t.string]), source: t.union([t.literal('user'), t.literal('backup')]), - recipients: t.array(t.any), + recipients: t.union([t.undefined, t.array(t.any)]), numBlocks: t.union([t.undefined, t.number]), feeRate: t.union([t.undefined, t.number]), feeMultiplier: t.union([t.undefined, t.number]), @@ -98,6 +107,11 @@ export const SendManyRequest = { export const SendManyResponse: HttpResponse = { // TODO: Get type from public types repo / Wallet Platform 200: t.any, + 400: t.any, + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -115,7 +129,12 @@ export const ConsolidateRequest = { // Response type for /consolidate endpoint const ConsolidateResponse: HttpResponse = { 200: t.any, + 202: t.any, 400: t.any, // All failed + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -140,6 +159,14 @@ const AccelerateResponse: HttpResponse = { txid: t.string, tx: t.string, }), + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.any, + }), 500: t.type({ error: t.string, details: t.string, @@ -152,6 +179,14 @@ const RecoveryWalletResponse: HttpResponse = { 200: t.type({ txHex: t.string, // the full signed transaction hex }), + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -212,6 +247,10 @@ const ConsolidateUnspentsResponse: HttpResponse = { txid: t.string, }), 400: t.any, + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, diff --git a/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts b/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts index e52e111..41b641a 100644 --- a/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts +++ b/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts @@ -39,6 +39,10 @@ const IndependentKeyRequest = { const IndependentKeyResponse: HttpResponse = { // TODO: Define proper response type 200: t.any, + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -56,6 +60,14 @@ const SignMultisigRequest = { const SignMultisigResponse: HttpResponse = { // TODO: Define proper response type for signed multisig transaction 200: t.any, + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -76,6 +88,14 @@ const RecoveryMultisigResponse: HttpResponse = { 200: t.type({ txHex: t.string, }), // the full signed tx + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -137,6 +157,14 @@ const SignMpcResponse: HttpResponse = { signatureShareRound3: t.any, }), ]), + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -322,6 +350,14 @@ export const EnclavedAPiSpec = apiSpec({ }), response: { 200: t.type(MpcInitializeResponse), + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -363,6 +399,14 @@ export const EnclavedAPiSpec = apiSpec({ }), response: { 200: MpcFinalizeResponseType, + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -381,6 +425,14 @@ export const EnclavedAPiSpec = apiSpec({ }), response: { 200: MpcV2InitializeResponseType, + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -399,6 +451,14 @@ export const EnclavedAPiSpec = apiSpec({ }), response: { 200: MpcV2RoundResponseType, + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, @@ -417,6 +477,14 @@ export const EnclavedAPiSpec = apiSpec({ }), response: { 200: MpcV2FinalizeResponseType, + 400: t.type({ + error: t.string, + details: t.string, + }), + 404: t.type({ + error: t.string, + details: t.string, + }), 500: t.type({ error: t.string, details: t.string, diff --git a/src/shared/responseHandler.ts b/src/shared/responseHandler.ts index a00c740..f5853e6 100644 --- a/src/shared/responseHandler.ts +++ b/src/shared/responseHandler.ts @@ -1,7 +1,7 @@ import { Request, Response as ExpressResponse, NextFunction } from 'express'; import { Config } from '../shared/types'; import { BitGoRequest } from '../types/request'; -import { EnclavedError } from '../errors'; +import { ApiResponseError } from 'bitgo'; // Extend Express Response to include sendEncoded interface EncodedResponse extends ExpressResponse { @@ -38,21 +38,31 @@ export function responseHandler(fn: ServiceFunction= 400) { + const body = { + error: errorObj.name || errorObj.error || 'Internal Server Error', + details: errorObj.details || errorObj.message || String(error), + }; + return res.sendEncoded(status, body); + } + + // For non-error status codes, return the error object as-is + const body = errorObj.result || errorObj.body || errorObj; + return res.sendEncoded(status, body); } }; }