diff --git a/packages/cardano-graphql-services/package.json b/packages/cardano-graphql-services/package.json index 33bc448deb4..55e3fe606c6 100644 --- a/packages/cardano-graphql-services/package.json +++ b/packages/cardano-graphql-services/package.json @@ -57,6 +57,7 @@ "express": "^4.17.3", "graphql-request": "npm:graphql-request-configurable-serializer@4.0.0", "reflect-metadata": "~0.1.13", + "serialize-error": "^8", "ts-log": "^2.2.4", "type-graphql": "~1.1.1" }, diff --git a/packages/cardano-graphql-services/src/TxSubmit/TxSubmitHttpServer.ts b/packages/cardano-graphql-services/src/TxSubmit/TxSubmitHttpServer.ts index bc8c20f68a0..e97a24e2c5a 100644 --- a/packages/cardano-graphql-services/src/TxSubmit/TxSubmitHttpServer.ts +++ b/packages/cardano-graphql-services/src/TxSubmit/TxSubmitHttpServer.ts @@ -1,6 +1,7 @@ +import { Cardano, TxSubmitProvider } from '@cardano-sdk/core'; import { HttpServer, ListenConfig } from '../Http'; import { Logger, dummyLogger } from 'ts-log'; -import { TxSubmitProvider } from '@cardano-sdk/core'; +import { serializeError } from 'serialize-error'; import express, { Router } from 'express'; const bodyParser = require('body-parser'); @@ -25,7 +26,6 @@ export class TxSubmitHttpServer extends HttpServer { } catch (error) { logger.error(error); body = error.message; - // Todo: Inspect errors, and set code based on type of error. res.statusCode = 500; } res.send(body); @@ -41,10 +41,9 @@ export class TxSubmitHttpServer extends HttpServer { await txSubmitProvider.submitTx(new Uint8Array(req.body)); body = undefined; } catch (error) { - logger.error(error); - body = error.message; - // Todo: Inspect errors, and set code based on type of error. - res.statusCode = 500; + res.statusCode = Cardano.util.hasTxSubmissionError(error) ? 400 : 500; + body = Array.isArray(error) ? error.map((e) => serializeError(e)) : serializeError(error); + logger.error(body); } res.send(body); }); diff --git a/packages/cardano-graphql-services/test/TxSubmit/TxSubmitHttpServer.test.ts b/packages/cardano-graphql-services/test/TxSubmit/TxSubmitHttpServer.test.ts index 025636e25ce..e45bdc17d47 100644 --- a/packages/cardano-graphql-services/test/TxSubmit/TxSubmitHttpServer.test.ts +++ b/packages/cardano-graphql-services/test/TxSubmit/TxSubmitHttpServer.test.ts @@ -1,10 +1,11 @@ +import { Cardano, TxSubmitProvider } from '@cardano-sdk/core'; import { TxSubmitHttpServer } from '../../src'; -import { TxSubmitProvider } from '@cardano-sdk/core'; import { getPort } from 'get-port-please'; import cbor from 'cbor'; import got from 'got'; const tx = cbor.encode('#####'); +const BAD_REQUEST_STRING = 'Response code 400 (Bad Request)'; describe('TxSubmitHttpServer', () => { let txSubmitProvider: TxSubmitProvider; @@ -16,61 +17,103 @@ describe('TxSubmitHttpServer', () => { port = await getPort(); apiUrlBase = `http://localhost:${port}`; txSubmitProvider = { healthCheck: jest.fn(() => Promise.resolve({ ok: true })), submitTx: jest.fn() }; - txSubmitHttpServer = TxSubmitHttpServer.create({ port }, { txSubmitProvider }); - await txSubmitHttpServer.initialize(); - await txSubmitHttpServer.start(); - }); - - afterAll(async () => { - await txSubmitHttpServer.shutdown(); }); afterEach(async () => { jest.resetAllMocks(); }); - it('health', async () => { - const res = await got(`${apiUrlBase}/health`, { - // eslint-disable-next-line sonarjs/no-duplicate-string - headers: { 'Content-Type': 'application/json' } + describe('healthy and successful submission', () => { + beforeAll(async () => { + txSubmitHttpServer = TxSubmitHttpServer.create({ port }, { txSubmitProvider }); + await txSubmitHttpServer.initialize(); + await txSubmitHttpServer.start(); }); - expect(res.statusCode).toBe(200); - expect(JSON.parse(res.body)).toEqual({ ok: true }); - }); - describe('submit', () => { - it('returns a 200 coded response with a well formed HTTP request', async () => { - const res = await got.post(`${apiUrlBase}/submit`, { - body: tx, - headers: { 'Content-Type': 'application/cbor' }, - method: 'post' - }); - expect(res.statusCode).toBe(200); - expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(1); + afterAll(async () => { + await txSubmitHttpServer.shutdown(); }); - it('returns a 400 coded response if the wrong content type header is used', async () => { - try { - await got.post(`${apiUrlBase}/submit`, { - body: tx, + describe('/health', () => { + it('forwards the txSubmitProvider health response', async () => { + const res = await got(`${apiUrlBase}/health`, { + // eslint-disable-next-line sonarjs/no-duplicate-string headers: { 'Content-Type': 'application/json' } }); - } catch (error) { - expect(error.message).toBe('Response code 400 (Bad Request)'); - expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(0); - } + expect(res.statusCode).toBe(200); + expect(JSON.parse(res.body)).toEqual({ ok: true }); + }); }); - it('returns a 400 coded response if the tx is not sent as binary data', async () => { - try { - await got.post(`${apiUrlBase}/submit`, { - body: Buffer.from(new Uint8Array()).toString(), - headers: { 'Content-Type': 'application/json' } - }); - } catch (error) { - expect(error.message).toBe('Response code 400 (Bad Request)'); - expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(0); - } + describe('/submit', () => { + it('returns a 200 coded response with a well formed HTTP request', async () => { + expect( + ( + await got.post(`${apiUrlBase}/submit`, { + body: tx, + headers: { 'Content-Type': 'application/cbor' }, + method: 'post' + }) + ).statusCode + ).toEqual(200); + expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(1); + }); + + it('returns a 400 coded response if the wrong content type header is used', async () => { + try { + await got.post(`${apiUrlBase}/submit`, { + body: tx, + headers: { 'Content-Type': 'application/json' } + }); + throw new Error('fail'); + } catch (error) { + expect(error.response.statusCode).toBe(400); + expect(error.message).toBe(BAD_REQUEST_STRING); + expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(0); + } + }); + + it('returns a 400 coded response if the tx is not sent as binary data', async () => { + try { + await got.post(`${apiUrlBase}/submit`, { + body: '', + headers: { 'Content-Type': 'application/json' } + }); + throw new Error('fail'); + } catch (error) { + expect(error.response.statusCode).toBe(400); + expect(error.message).toBe(BAD_REQUEST_STRING); + expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(0); + } + }); + }); + }); + + describe('healthy but failing submission', () => { + describe('/submit', () => { + // eslint-disable-next-line max-len + it('returns a 400 coded response with detail in the body to a transaction containing a domain violation', async () => { + const stubErrors = [new Cardano.TxSubmissionErrors.BadInputsError({ badInputs: [] })]; + txSubmitProvider = { + healthCheck: jest.fn(() => Promise.resolve({ ok: true })), + submitTx: jest.fn(() => Promise.reject(stubErrors)) + }; + txSubmitHttpServer = TxSubmitHttpServer.create({ port }, { txSubmitProvider }); + await txSubmitHttpServer.initialize(); + await txSubmitHttpServer.start(); + try { + await got.post(`${apiUrlBase}/submit`, { + body: Buffer.from(new Uint8Array()).toString(), + headers: { 'Content-Type': 'application/cbor' } + }); + throw new Error('fail'); + } catch (error) { + expect(error.response.statusCode).toBe(400); + expect(JSON.parse(error.response.body)[0].name).toEqual(stubErrors[0].name); + expect(txSubmitProvider.submitTx).toHaveBeenCalledTimes(1); + await txSubmitHttpServer.shutdown(); + } + }); }); }); }); diff --git a/yarn.lock b/yarn.lock index 628bdf1e527..8d37e0ab656 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8851,6 +8851,13 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" +serialize-error@^8: + version "8.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" + integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== + dependencies: + type-fest "^0.20.2" + serve-static@1.14.2: version "1.14.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa"