Skip to content

Commit

Permalink
feat: block height API
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Jan 30, 2024
1 parent 322db1e commit 00556c2
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 1 deletion.
68 changes: 68 additions & 0 deletions lib/api/v2/routers/ChainRouter.ts
Expand Up @@ -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:
Expand All @@ -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}:
Expand Down Expand Up @@ -163,13 +221,23 @@ 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, {
fee: (await this.service.getFeeEstimation(currency)).get(currency),
});
};

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, [
Expand Down
26 changes: 26 additions & 0 deletions lib/service/Service.ts
Expand Up @@ -572,6 +572,32 @@ class Service {
throw Errors.CURRENCY_NOT_FOUND(symbol);
};

public getBlockHeights = async (
symbol?: string,
): Promise<Map<string, number>> => {
const currencies = symbol
? [this.getCurrency(symbol)]
: Array.from(this.currencies.values());

return new Map<string, number>(
(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
*/
Expand Down
81 changes: 81 additions & 0 deletions swagger-spec.json
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
}
}
}
}
}
Expand Down
54 changes: 53 additions & 1 deletion test/unit/api/v2/routers/ChainRouter.spec.ts
Expand Up @@ -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);
Expand All @@ -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(),
Expand All @@ -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';

Expand All @@ -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';
Expand Down
24 changes: 24 additions & 0 deletions test/unit/service/Service.spec.ts
Expand Up @@ -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();
Expand Down

0 comments on commit 00556c2

Please sign in to comment.