From 85289bdd70ffb1d06dd4dea8aeeec28f653ebb3d Mon Sep 17 00:00:00 2001 From: michael1011 Date: Mon, 22 Jan 2024 12:33:23 +0100 Subject: [PATCH] feat: API v2 swap status --- lib/api/Api.ts | 2 +- lib/api/Controller.ts | 8 +- lib/api/Utils.ts | 4 +- lib/api/v2/ApiV2.ts | 12 +-- lib/api/v2/routers/SwapRouter.ts | 84 +++++++++++++++++++++ swagger-spec.json | 72 ++++++++++++++++++ test/unit/api/v2/ApiV2.spec.ts | 2 +- test/unit/api/v2/routers/SwapRouter.spec.ts | 48 +++++++++++- 8 files changed, 218 insertions(+), 14 deletions(-) diff --git a/lib/api/Api.ts b/lib/api/Api.ts index 36e60cb0..e6af272f 100644 --- a/lib/api/Api.ts +++ b/lib/api/Api.ts @@ -44,7 +44,7 @@ class Api { }, ); - new ApiV2(this.logger, service).registerRoutes(this.app); + new ApiV2(this.logger, service, this.controller).registerRoutes(this.app); this.registerRoutes(this.controller); } diff --git a/lib/api/Controller.ts b/lib/api/Controller.ts index b5e308f2..2e52e99b 100644 --- a/lib/api/Controller.ts +++ b/lib/api/Controller.ts @@ -31,12 +31,12 @@ import { } from './Utils'; class Controller { - // A map between the ids and HTTP streams of all pending swaps - private pendingSwapStreams = new Map(); - // TODO: refactor // A map between the ids and statuses of the swaps - private pendingSwapInfos = new Map(); + public pendingSwapInfos = new Map(); + + // A map between the ids and HTTP streams of all pending swaps + private pendingSwapStreams = new Map(); constructor( private logger: Logger, diff --git a/lib/api/Utils.ts b/lib/api/Utils.ts index c26a6a9d..ffe394ec 100644 --- a/lib/api/Utils.ts +++ b/lib/api/Utils.ts @@ -125,7 +125,9 @@ export const writeErrorResponse = ( if (!errorsNotToLog.includes(error?.error || error)) { logger.warn( `Request ${req.method} ${urlPrefix + req.url} ${ - Object.keys(req.body).length > 0 ? `${JSON.stringify(req.body)} ` : '' + req.body && Object.keys(req.body).length > 0 + ? `${JSON.stringify(req.body)} ` + : '' }failed: ${JSON.stringify(error)}`, ); } diff --git a/lib/api/v2/ApiV2.ts b/lib/api/v2/ApiV2.ts index 78635271..67bfc236 100644 --- a/lib/api/v2/ApiV2.ts +++ b/lib/api/v2/ApiV2.ts @@ -1,6 +1,7 @@ import { Application } from 'express'; import Logger from '../../Logger'; import Service from '../../service/Service'; +import Controller from '../Controller'; import { apiPrefix } from './Consts'; import ChainRouter from './routers/ChainRouter'; import InfoRouter from './routers/InfoRouter'; @@ -13,13 +14,14 @@ class ApiV2 { constructor( private readonly logger: Logger, - private readonly service: Service, + service: Service, + controller: Controller, ) { this.routers = [ - new InfoRouter(this.logger, this.service), - new SwapRouter(this.logger, this.service), - new ChainRouter(this.logger, this.service), - new NodesRouter(this.logger, this.service), + new InfoRouter(this.logger, service), + new SwapRouter(this.logger, service, controller), + new ChainRouter(this.logger, service), + new NodesRouter(this.logger, service), ]; } diff --git a/lib/api/v2/routers/SwapRouter.ts b/lib/api/v2/routers/SwapRouter.ts index ea41a4ea..3eb147ca 100644 --- a/lib/api/v2/routers/SwapRouter.ts +++ b/lib/api/v2/routers/SwapRouter.ts @@ -4,9 +4,11 @@ import { getHexString, stringify } from '../../../Utils'; import { SwapVersion } from '../../../consts/Enums'; import RateProviderTaproot from '../../../rates/providers/RateProviderTaproot'; import Service from '../../../service/Service'; +import Controller from '../../Controller'; import { checkPreimageHashLength, createdResponse, + errorResponse, successResponse, validateRequest, } from '../../Utils'; @@ -16,6 +18,7 @@ class SwapRouter extends RouterBase { constructor( logger: Logger, private readonly service: Service, + private readonly controller: Controller, ) { super(logger, 'swap'); } @@ -51,6 +54,67 @@ class SwapRouter extends RouterBase { const router = Router(); + /** + * @openapi + * tags: + * name: Swap + * description: Generic Swap related endpoints + */ + + /** + * @openapi + * components: + * schemas: + * SwapStatus: + * type: object + * properties: + * status: + * type: string + * description: Status of the Swap + * zeroConfRejected: + * type: boolean + * description: Whether 0-conf was accepted for the lockup transaction of the Submarine Swap + * transaction: + * type: object + * description: Details of the lockup transaction of a Reverse Swap + * properties: + * id: + * type: string + * description: ID of the transaction + * hex: + * type: string + * description: Raw hex of the transaction + */ + + /** + * @openapi + * /swap/{id}: + * get: + * tags: [Swap] + * description: Get the status of a Swap + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: ID of the Swap + * responses: + * '200': + * description: The latest status of the Swap + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SwapStatus' + * '404': + * description: When no Swap with the ID could be found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ + router.get('/:id', this.handleError(this.getSwapStatus)); + /** * @openapi * tags: @@ -548,6 +612,26 @@ class SwapRouter extends RouterBase { return router; }; + private getSwapStatus = (req: Request, res: Response) => { + const { id } = validateRequest(req.params, [ + { name: 'id', type: 'string' }, + ]); + + const response = this.controller.pendingSwapInfos.get(id); + + if (response) { + successResponse(res, response); + } else { + errorResponse( + this.logger, + req, + res, + `could not find swap with id: ${id}`, + 404, + ); + } + }; + private getSubmarine = (_req: Request, res: Response) => successResponse( res, diff --git a/swagger-spec.json b/swagger-spec.json index 1790cd71..6ed6b3a7 100644 --- a/swagger-spec.json +++ b/swagger-spec.json @@ -310,6 +310,47 @@ } } }, + "/swap/{id}": { + "get": { + "tags": [ + "Swap" + ], + "description": "Get the status of a Swap", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of the Swap" + } + ], + "responses": { + "200": { + "description": "The latest status of the Swap", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwapStatus" + } + } + } + }, + "404": { + "description": "When no Swap with the ID could be found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, "/swap/submarine": { "get": { "description": "Possible pairs for Submarine Swaps", @@ -626,6 +667,33 @@ } } }, + "SwapStatus": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status of the Swap" + }, + "zeroConfRejected": { + "type": "boolean", + "description": "Whether 0-conf was accepted for the lockup transaction of the Submarine Swap" + }, + "transaction": { + "type": "object", + "description": "Details of the lockup transaction of a Reverse Swap", + "properties": { + "id": { + "type": "string", + "description": "ID of the transaction" + }, + "hex": { + "type": "string", + "description": "Raw hex of the transaction" + } + } + } + } + }, "SubmarinePair": { "type": "object", "properties": { @@ -967,6 +1035,10 @@ "name": "Nodes", "description": "Lightning nodes" }, + { + "name": "Swap", + "description": "Generic Swap related endpoints" + }, { "name": "Submarine", "description": "Submarine Swap related endpoints" diff --git a/test/unit/api/v2/ApiV2.spec.ts b/test/unit/api/v2/ApiV2.spec.ts index 8f572a3a..3095be0b 100644 --- a/test/unit/api/v2/ApiV2.spec.ts +++ b/test/unit/api/v2/ApiV2.spec.ts @@ -57,7 +57,7 @@ describe('ApiV2', () => { use: jest.fn(), } as any; - new ApiV2(Logger.disabledLogger, {} as any).registerRoutes(app); + new ApiV2(Logger.disabledLogger, {} as any, {} as any).registerRoutes(app); expect(mockGetInfoRouter).toHaveBeenCalledTimes(1); expect(mockSwapGetRouter).toHaveBeenCalledTimes(1); diff --git a/test/unit/api/v2/routers/SwapRouter.spec.ts b/test/unit/api/v2/routers/SwapRouter.spec.ts index 3bb72a3c..cf6c9ae0 100644 --- a/test/unit/api/v2/routers/SwapRouter.spec.ts +++ b/test/unit/api/v2/routers/SwapRouter.spec.ts @@ -2,6 +2,7 @@ import { randomBytes } from 'crypto'; import { Router } from 'express'; import Logger from '../../../../../lib/Logger'; import { getHexBuffer, getHexString } from '../../../../../lib/Utils'; +import Controller from '../../../../../lib/api/Controller'; import SwapRouter from '../../../../../lib/api/v2/routers/SwapRouter'; import { OrderSide, SwapVersion } from '../../../../../lib/consts/Enums'; import RateProviderTaproot from '../../../../../lib/rates/providers/RateProviderTaproot'; @@ -64,7 +65,11 @@ describe('SwapRouter', () => { }), } as unknown as Service; - const swapRouter = new SwapRouter(Logger.disabledLogger, service); + const controller = { + pendingSwapInfos: new Map([['swapId', { some: 'statusData' }]]), + } as unknown as Controller; + + const swapRouter = new SwapRouter(Logger.disabledLogger, service, controller); beforeEach(() => { jest.clearAllMocks(); @@ -80,7 +85,8 @@ describe('SwapRouter', () => { expect(Router).toHaveBeenCalledTimes(1); - expect(mockedRouter.get).toHaveBeenCalledTimes(3); + expect(mockedRouter.get).toHaveBeenCalledTimes(4); + expect(mockedRouter.get).toHaveBeenCalledWith('/:id', expect.anything()); expect(mockedRouter.get).toHaveBeenCalledWith( '/submarine', expect.anything(), @@ -113,6 +119,44 @@ describe('SwapRouter', () => { ); }); + test.each` + error | params + ${'undefined parameter: id'} | ${{}} + ${'invalid parameter: id'} | ${{ id: 1 }} + `( + 'should not get status of swaps with invalid parameters ($error)', + ({ params, error }) => { + expect(() => + swapRouter['getSwapStatus']( + mockRequest(undefined, undefined, params), + mockResponse(), + ), + ).toThrow(error); + }, + ); + + test('should get status of swaps', () => { + const id = 'swapId'; + + const res = mockResponse(); + swapRouter['getSwapStatus'](mockRequest(undefined, undefined, { id }), res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(controller.pendingSwapInfos.get(id)); + }); + + test('should return 404 as status when swap id cannot be found', () => { + const id = 'notFound'; + + const res = mockResponse(); + swapRouter['getSwapStatus'](mockRequest(undefined, undefined, { id }), res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + error: `could not find swap with id: ${id}`, + }); + }); + test('should get submarine pairs', () => { const res = mockResponse(); swapRouter['getSubmarine'](mockRequest(), res);