diff --git a/lib/api/v2/routers/ChainRouter.ts b/lib/api/v2/routers/ChainRouter.ts index 1eeb548c..c64cdca9 100644 --- a/lib/api/v2/routers/ChainRouter.ts +++ b/lib/api/v2/routers/ChainRouter.ts @@ -120,6 +120,7 @@ class ChainRouter extends RouterBase { * parameters: * - in: path * name: currency + * required: true * schema: * type: string * description: Currency of the chain to broadcast on @@ -134,8 +135,8 @@ class ChainRouter extends RouterBase { * type: string * description: The transaction to broadcast as raw HEX * responses: - * '200': - * description: Id of the broadcast transaction + * '201': + * description: ID of the broadcast transaction * content: * application/json: * schema: @@ -143,7 +144,7 @@ class ChainRouter extends RouterBase { * properties: * id: * type: string - * description: Id of the broadcast transaction + * description: ID of the broadcast transaction * '400': * description: Error that caused the broadcast of the transaction to fail * content: diff --git a/lib/api/v2/routers/SwapRouter.ts b/lib/api/v2/routers/SwapRouter.ts index b07b92c7..ea41a4ea 100644 --- a/lib/api/v2/routers/SwapRouter.ts +++ b/lib/api/v2/routers/SwapRouter.ts @@ -206,6 +206,59 @@ class SwapRouter extends RouterBase { */ router.post('/submarine', this.handleError(this.createSubmarine)); + /** + * @openapi + * components: + * schemas: + * SubmarineTransaction: + * type: object + * properties: + * id: + * type: string + * description: ID the lockup transaction + * hex: + * type: string + * description: Lockup transaction as raw HEX + * timeoutBlockHeight: + * type: number + * description: Block height at which the time-lock expires + * timeoutEta: + * type: number + * description: UNIX timestamp at which the time-lock expires; set if it has not expired already + */ + + /** + * @openapi + * /swap/submarine/{id}/transaction: + * get: + * tags: [Submarine] + * description: Get the lockup transaction of a Submarine Swap + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: ID of the Submarine Swap + * responses: + * '200': + * description: The lockup transaction of the Submarine Swap and accompanying information + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubmarineTransaction' + * '400': + * description: Error that caused the request to fail + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ + router.get( + '/submarine/:id/transaction', + this.handleError(this.getSubmarineTransaction), + ); + /** * @openapi * components: @@ -533,6 +586,21 @@ class SwapRouter extends RouterBase { createdResponse(res, response); }; + private getSubmarineTransaction = async (req: Request, res: Response) => { + const { id } = validateRequest(req.params, [ + { name: 'id', type: 'string' }, + ]); + + const { transactionHex, transactionId, timeoutBlockHeight, timeoutEta } = + await this.service.getSwapTransaction(id); + successResponse(res, { + id: transactionId, + hex: transactionHex, + timeoutBlockHeight, + timeoutEta, + }); + }; + private refundSubmarine = async (req: Request, res: Response) => { const { id, pubNonce, index, transaction } = validateRequest(req.body, [ { name: 'id', type: 'string' }, diff --git a/lib/service/Service.ts b/lib/service/Service.ts index 982f963e..2e55535c 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -88,6 +88,13 @@ type Contracts = { rsk?: NetworkContracts; }; +type SwapTransaction = { + transactionId: string; + timeoutBlockHeight: number; + transactionHex?: string; + timeoutEta?: number; +}; + class Service { public allowReverseSwaps = true; @@ -487,12 +494,7 @@ class Service { * Gets the hex encoded lockup transaction of a Submarine Swap, the block height * at which it will time out and the expected ETA for that block */ - public getSwapTransaction = async ( - id: string, - ): Promise<{ - transactionHex: string; - timeoutBlockHeight: number; - }> => { + public getSwapTransaction = async (id: string): Promise => { const swap = await SwapRepository.getSwap({ id, }); @@ -510,7 +512,7 @@ class Service { const currency = this.getCurrency(chainCurrency); - const response: any = { + const response: SwapTransaction = { transactionId: swap.lockupTransactionId, timeoutBlockHeight: swap.timeoutBlockHeight, }; diff --git a/swagger-spec.json b/swagger-spec.json index 794e8588..1790cd71 100644 --- a/swagger-spec.json +++ b/swagger-spec.json @@ -132,6 +132,7 @@ { "in": "path", "name": "currency", + "required": true, "schema": { "type": "string" }, @@ -155,8 +156,8 @@ } }, "responses": { - "200": { - "description": "Id of the broadcast transaction", + "201": { + "description": "ID of the broadcast transaction", "content": { "application/json": { "schema": { @@ -164,7 +165,7 @@ "properties": { "id": { "type": "string", - "description": "Id of the broadcast transaction" + "description": "ID of the broadcast transaction" } } } @@ -373,6 +374,47 @@ } } }, + "/swap/submarine/{id}/transaction": { + "get": { + "tags": [ + "Submarine" + ], + "description": "Get the lockup transaction of a Submarine Swap", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of the Submarine Swap" + } + ], + "responses": { + "200": { + "description": "The lockup transaction of the Submarine Swap and accompanying information", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmarineTransaction" + } + } + } + }, + "400": { + "description": "Error that caused the request to fail", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, "/swap/submarine/refund": { "post": { "description": "Requests a partial signature for a cooperative Submarine Swap refund transaction", @@ -698,6 +740,27 @@ } } }, + "SubmarineTransaction": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID the lockup transaction" + }, + "hex": { + "type": "string", + "description": "Lockup transaction as raw HEX" + }, + "timeoutBlockHeight": { + "type": "number", + "description": "Block height at which the time-lock expires" + }, + "timeoutEta": { + "type": "number", + "description": "UNIX timestamp at which the time-lock expires; set if it has not expired already" + } + } + }, "SubmarineRefundRequest": { "type": "object", "properties": { diff --git a/test/unit/api/v2/routers/SwapRouter.spec.ts b/test/unit/api/v2/routers/SwapRouter.spec.ts index 2e85e936..3bb72a3c 100644 --- a/test/unit/api/v2/routers/SwapRouter.spec.ts +++ b/test/unit/api/v2/routers/SwapRouter.spec.ts @@ -56,6 +56,12 @@ describe('SwapRouter', () => { .mockReturnValue({ pairId: 'L-BTC/BTC', orderSide: OrderSide.BUY }), createSwapWithInvoice: jest.fn().mockResolvedValue({ id: 'randomId' }), createReverseSwap: jest.fn().mockResolvedValue({ id: 'reverseId' }), + getSwapTransaction: jest.fn().mockResolvedValue({ + transactionId: 'txId', + transactionHex: 'txHex', + timeoutBlockHeight: 21, + timeoutEta: 210987, + }), } as unknown as Service; const swapRouter = new SwapRouter(Logger.disabledLogger, service); @@ -74,11 +80,15 @@ describe('SwapRouter', () => { expect(Router).toHaveBeenCalledTimes(1); - expect(mockedRouter.get).toHaveBeenCalledTimes(2); + expect(mockedRouter.get).toHaveBeenCalledTimes(3); expect(mockedRouter.get).toHaveBeenCalledWith( '/submarine', expect.anything(), ); + expect(mockedRouter.get).toHaveBeenCalledWith( + '/submarine/:id/transaction', + expect.anything(), + ); expect(mockedRouter.get).toHaveBeenCalledWith( '/reverse', expect.anything(), @@ -197,8 +207,8 @@ describe('SwapRouter', () => { referralId: 'partner', refundPublicKey: '0021', }; - const res = mockResponse(); + const res = mockResponse(); await swapRouter['createSubmarine'](mockRequest(reqBody), res); expect(service.createSwapWithInvoice).toHaveBeenCalledTimes(1); @@ -214,6 +224,43 @@ describe('SwapRouter', () => { ); }); + test.each` + error | params + ${'undefined parameter: id'} | ${{}} + ${'invalid parameter: id'} | ${{ id: 1 }} + `( + 'should not get lockup transaction of submarine swaps with invalid parameters ($error)', + async ({ error, params }) => { + await expect( + swapRouter['getSubmarineTransaction']( + mockRequest(undefined, undefined, params), + mockResponse(), + ), + ).rejects.toEqual(error); + }, + ); + + test('should get lockup transaction of submarine swaps', async () => { + const id = 'asdf'; + + const res = mockResponse(); + await swapRouter['getSubmarineTransaction']( + mockRequest(undefined, undefined, { id }), + res, + ); + + expect(service.getSwapTransaction).toHaveBeenCalledTimes(1); + expect(service.getSwapTransaction).toHaveBeenCalledWith(id); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + id: 'txId', + hex: 'txHex', + timeoutBlockHeight: 21, + timeoutEta: 210987, + }); + }); + test.each` error | body ${'undefined parameter: id'} | ${{}}