diff --git a/lib/api/v2/routers/ChainRouter.ts b/lib/api/v2/routers/ChainRouter.ts index c64cdca9..49bd7b46 100644 --- a/lib/api/v2/routers/ChainRouter.ts +++ b/lib/api/v2/routers/ChainRouter.ts @@ -42,6 +42,25 @@ class ChainRouter extends RouterBase { */ router.get('/fees', this.handleError(this.getFees)); + /** + * @openapi + * /chain/heights: + * get: + * description: Block heights for all supported chains + * tags: [Chain] + * responses: + * '200': + * description: Object of currency of chain -> block height + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: number + * description: Block height of the chain + */ + router.get('/heights', this.handleError(this.getHeights)); + /** * @openapi * /chain/{currency}/fee: @@ -66,9 +85,48 @@ class ChainRouter extends RouterBase { * fee: * type: number * description: Fee estimation in sat/vbyte or GWEI + * '400': + * description: Error that caused the request to fail + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' */ router.get('/:currency/fee', this.handleError(this.getFeeForChain)); + /** + * @openapi + * /chain/{currency}/height: + * get: + * description: Block height for a chain + * tags: [Chain] + * parameters: + * - in: path + * name: currency + * required: true + * schema: + * type: string + * description: Currency of the chain to get the block height for + * responses: + * '200': + * description: Object containing the block height + * content: + * application/json: + * schema: + * type: object + * properties: + * fee: + * type: number + * description: Block height of the chain + * '400': + * description: Error that caused the request to fail + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ + router.get('/:currency/height', this.handleError(this.getHeightForChain)); + /** * @openapi * /chain/{currency}/transaction/{id}: @@ -163,6 +221,9 @@ class ChainRouter extends RouterBase { private getFees = async (_: Request, res: Response) => successResponse(res, mapToObject(await this.service.getFeeEstimation())); + private getHeights = async (_: Request, res: Response) => + successResponse(res, mapToObject(await this.service.getBlockHeights())); + private getFeeForChain = async (req: Request, res: Response) => { const currency = this.getCurrencyFromPath(req); successResponse(res, { @@ -170,6 +231,13 @@ class ChainRouter extends RouterBase { }); }; + private getHeightForChain = async (req: Request, res: Response) => { + const currency = this.getCurrencyFromPath(req); + successResponse(res, { + height: (await this.service.getBlockHeights(currency)).get(currency), + }); + }; + private getTransaction = async (req: Request, res: Response) => { const currency = this.getCurrencyFromPath(req); const { id } = validateRequest(req.params, [ diff --git a/lib/service/Service.ts b/lib/service/Service.ts index f8f7c777..30dbe79d 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -572,6 +572,32 @@ class Service { throw Errors.CURRENCY_NOT_FOUND(symbol); }; + public getBlockHeights = async ( + symbol?: string, + ): Promise> => { + const currencies = symbol + ? [this.getCurrency(symbol)] + : Array.from(this.currencies.values()); + + return new Map( + (await Promise.all( + currencies.map(async (currency) => { + if (currency.chainClient) { + return [ + currency.symbol, + (await currency.chainClient.getBlockchainInfo()).blocks, + ]; + } else { + return [ + currency.symbol, + (await currency.provider?.getBlockNumber()) || 0, + ]; + } + }), + )) as [string, number][], + ); + }; + /** * Gets a fee estimation in satoshis per vbyte or GWEI for either all currencies or just a single one if specified */ diff --git a/swagger-spec.json b/swagger-spec.json index d293669b..fa03f7ec 100644 --- a/swagger-spec.json +++ b/swagger-spec.json @@ -29,6 +29,30 @@ } } }, + "/chain/heights": { + "get": { + "description": "Block heights for all supported chains", + "tags": [ + "Chain" + ], + "responses": { + "200": { + "description": "Object of currency of chain -> block height", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "number", + "description": "Block height of the chain" + } + } + } + } + } + } + } + }, "/chain/{currency}/fee": { "get": { "description": "Fee estimations for a chain", @@ -62,6 +86,63 @@ } } } + }, + "400": { + "description": "Error that caused the request to fail", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/chain/{currency}/height": { + "get": { + "description": "Block height for a chain", + "tags": [ + "Chain" + ], + "parameters": [ + { + "in": "path", + "name": "currency", + "required": true, + "schema": { + "type": "string" + }, + "description": "Currency of the chain to get the block height for" + } + ], + "responses": { + "200": { + "description": "Object containing the block height", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "fee": { + "type": "number", + "description": "Block height of the chain" + } + } + } + } + } + }, + "400": { + "description": "Error that caused the request to fail", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } diff --git a/test/unit/api/v2/routers/ChainRouter.spec.ts b/test/unit/api/v2/routers/ChainRouter.spec.ts index b030deaf..ab39816b 100644 --- a/test/unit/api/v2/routers/ChainRouter.spec.ts +++ b/test/unit/api/v2/routers/ChainRouter.spec.ts @@ -32,6 +32,17 @@ describe('ChainRouter', () => { ['RBTC', 23.121212312], ]); }), + getBlockHeights: jest.fn().mockImplementation(async (currency?: string) => { + if (currency) { + return new Map([['BTC', 210_00]]); + } + + return new Map([ + ['BTC', 210_00], + ['L-BTC', 2_100_000], + ['RBTC', 5_000_000], + ]); + }), } as unknown as Service; const chainRouter = new ChainRouter(Logger.disabledLogger, service); @@ -50,12 +61,20 @@ describe('ChainRouter', () => { expect(Router).toHaveBeenCalledTimes(1); - expect(mockedRouter.get).toHaveBeenCalledTimes(3); + expect(mockedRouter.get).toHaveBeenCalledTimes(5); expect(mockedRouter.get).toHaveBeenCalledWith('/fees', expect.anything()); + expect(mockedRouter.get).toHaveBeenCalledWith( + '/heights', + expect.anything(), + ); expect(mockedRouter.get).toHaveBeenCalledWith( '/:currency/fee', expect.anything(), ); + expect(mockedRouter.get).toHaveBeenCalledWith( + '/:currency/height', + expect.anything(), + ); expect(mockedRouter.get).toHaveBeenCalledWith( '/:currency/transaction/:id', expect.anything(), @@ -81,6 +100,19 @@ describe('ChainRouter', () => { ); }); + test('should get block heights', async () => { + const res = mockResponse(); + await chainRouter['getHeights'](mockRequest(), res); + + expect(service.getBlockHeights).toHaveBeenCalledTimes(1); + expect(service.getBlockHeights).toHaveBeenCalledWith(); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + mapToObject(await service.getBlockHeights()), + ); + }); + test('should get fee estimation for chain', async () => { const currency = 'BTC'; @@ -101,6 +133,26 @@ describe('ChainRouter', () => { }); }); + test('should get block height for chain', async () => { + const currency = 'BTC'; + + const res = mockResponse(); + await chainRouter['getHeightForChain']( + mockRequest(undefined, undefined, { + currency, + }), + res, + ); + + expect(service.getBlockHeights).toHaveBeenCalledTimes(1); + expect(service.getBlockHeights).toHaveBeenCalledWith(currency); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + height: (await service.getBlockHeights(currency)).get(currency), + }); + }); + test('should get transaction', async () => { const currency = 'BTC'; const id = '1234567890'; diff --git a/test/unit/service/Service.spec.ts b/test/unit/service/Service.spec.ts index 45692f99..63c23abe 100644 --- a/test/unit/service/Service.spec.ts +++ b/test/unit/service/Service.spec.ts @@ -1063,6 +1063,30 @@ describe('Service', () => { ); }); + test('should get block heights', async () => { + await expect(service.getBlockHeights()).resolves.toEqual( + new Map([ + ['BTC', 123], + ['LTC', 123], + ['ETH', 100], + ['USDT', 100], + ]), + ); + }); + + test('should get block height for symbol', async () => { + await expect(service.getBlockHeights('BTC')).resolves.toEqual( + new Map([['BTC', 123]]), + ); + }); + + test('should throw when getting block height for symbol that cannot be found', async () => { + const symbol = 'notFound'; + await expect(service.getBlockHeights(symbol)).rejects.toEqual( + Errors.CURRENCY_NOT_FOUND(symbol), + ); + }); + test('should get fee estimation', async () => { // Get fee estimation of all currencies const feeEstimation = await service.getFeeEstimation();