From 96f768e7e0c4d439b5e02871ac744d7187497b72 Mon Sep 17 00:00:00 2001 From: Nichelle <8347352+nichellekoh@users.noreply.github.com> Date: Wed, 20 Apr 2022 09:33:00 +0800 Subject: [PATCH] feat(status-api): Status API for Oracles (#1315) * oracles status setup * Refactor name to OracleStatusController and update tests for status * Update logic for operational status to less than equals to 15 minutes * Update Oracle status logic to outage if > 45 mins * Update oracle status path * Corrected to get last published time from price feed instead * Refactor oracle status test * Update readme file with new /oracles status * Introduce OracleStatus for explicit API response * Updated mocked oracle function name * Refactored OracleStatus * Refactor controller test dir * Refactor controller test dir * Update oracle address to path param * Update oracle address to path param * Update OracleStatusController.test.ts Co-authored-by: Joel-David Wong --- apps/status-api/README.md | 23 ++++- .../BlockchainStatusController.test.ts | 0 .../OracleStatusController.test.ts | 87 +++++++++++++++++++ .../OverallStatusController.test.ts | 0 .../src/controllers/OracleStatusController.ts | 51 +++++++++++ .../src/modules/ControllerModule.ts | 7 +- .../whale/src/module.api/oracle.controller.ts | 4 +- 7 files changed, 166 insertions(+), 6 deletions(-) rename apps/status-api/{__test__ => __tests__}/controllers/BlockchainStatusController.test.ts (100%) create mode 100644 apps/status-api/__tests__/controllers/OracleStatusController.test.ts rename apps/status-api/{__test__ => __tests__}/controllers/OverallStatusController.test.ts (100%) create mode 100644 apps/status-api/src/controllers/OracleStatusController.ts diff --git a/apps/status-api/README.md b/apps/status-api/README.md index e53e95d142..f42a445128 100644 --- a/apps/status-api/README.md +++ b/apps/status-api/README.md @@ -8,12 +8,29 @@ DeFiChain Status API, providing the statuses of different DeFiChain services. To decouple the DeFiChain products from the status page, the approach of having a centralised provider to determine each DeFiChain service status with a pre-defined logic will allow it to be maintained consistently throughout. This will allow other apps or services to share the status from the same Status APIs. -### `/blockchain/status` +### `/blockchain` >https://github.com/DeFiCh/jellyfish/issues/1271 -To provide the status of the blockchain based on the block creation time interval. +To provide the status of the blockchain based on the block creation time interval + +| Status | Threshold Time | +|--------------------|-------------------| +| `operational` | `< 30 minutes` | +| `degraded` | `30 - 45 minutes` | +| `outage` | `> 45 minutes` | + +### `/oracles/:address` + +To provide the status of each oracle given the address based on the last published time for any given token + + +| Status | Threshold Time | +|--------------------|------------------| +| `operational` | `<= 45 minutes` | +| `outage` | `> 45 minutes` | + -### `/overall/status` +### `/overall` >https://github.com/DeFiCh/jellyfish/issues/1274 To provide the aggregated status of all services required by Light Wallet & Scan (Blockchain & Ocean). diff --git a/apps/status-api/__test__/controllers/BlockchainStatusController.test.ts b/apps/status-api/__tests__/controllers/BlockchainStatusController.test.ts similarity index 100% rename from apps/status-api/__test__/controllers/BlockchainStatusController.test.ts rename to apps/status-api/__tests__/controllers/BlockchainStatusController.test.ts diff --git a/apps/status-api/__tests__/controllers/OracleStatusController.test.ts b/apps/status-api/__tests__/controllers/OracleStatusController.test.ts new file mode 100644 index 0000000000..c8658f79d9 --- /dev/null +++ b/apps/status-api/__tests__/controllers/OracleStatusController.test.ts @@ -0,0 +1,87 @@ +import { StatusApiTesting } from '../../testing/StatusApiTesting' +import { ApiPagedResponse, WhaleApiClient } from '@defichain/whale-api-client' +import { Oracle, OraclePriceFeed } from '@defichain/whale-api-client/dist/api/Oracles' + +const apiTesting = StatusApiTesting.create() + +beforeAll(async () => { + await apiTesting.start() + jest.spyOn(apiTesting.app.get(WhaleApiClient).oracles, 'getOracleByAddress') + .mockReturnValue(getMockedOracle()) +}) + +afterAll(async () => { + await apiTesting.stop() +}) + +describe('OracleStatusController - Status test', () => { + it('/oracles/
- should get operational as last published < 45 mins ago', async () => { + jest.spyOn(apiTesting.app.get(WhaleApiClient).oracles, 'getPriceFeed') + .mockReturnValueOnce(getMockedOraclePriceFeed('df1qm7f2cx8vs9lqn8v43034nvckz6dxxpqezfh6dw', 5)) + + const res = await apiTesting.app.inject({ + method: 'GET', + url: 'oracles/df1qm7f2cx8vs9lqn8v43034nvckz6dxxpqezfh6dw' + }) + expect(res.json()).toStrictEqual({ + status: 'operational' + }) + expect(res.statusCode).toStrictEqual(200) + }) + + it('/oracles/
- should get outage as last published >= 45 mins ago', async () => { + jest.spyOn(apiTesting.app.get(WhaleApiClient).oracles, 'getPriceFeed') + .mockReturnValueOnce(getMockedOraclePriceFeed('df1qcpp3entq53tdyklm5v0lnvqer4verr4puxchq4', 46)) + + const res = await apiTesting.app.inject({ + method: 'GET', + url: 'oracles/df1qcpp3entq53tdyklm5v0lnvqer4verr4puxchq4' + }) + expect(res.json()).toStrictEqual({ + status: 'outage' + }) + expect(res.statusCode).toStrictEqual(200) + }) +}) + +async function getMockedOraclePriceFeed (oracleAddress: string, minutesDiff: number): Promise> { + const blockMedianTime = Date.now() / 1000 - (minutesDiff * 60) + + return new ApiPagedResponse({ + data: [{ + block: { + medianTime: blockMedianTime, + hash: '', + height: 0, + time: 0 + }, + id: '', + key: '', + sort: '', + token: '', + currency: '', + oracleId: '', + txid: '', + time: 0, + amount: '' + }] + }, 'GET', `oracles/${oracleAddress}/AAPL-USD/feed`) +} + +async function getMockedOracle (): Promise { + return { + id: '', + block: { + hash: '', + height: 0, + medianTime: 0, + time: 0 + }, + ownerAddress: '', + priceFeeds: [{ + token: '', + currency: '' + }], + weightage: 0 + } +} diff --git a/apps/status-api/__test__/controllers/OverallStatusController.test.ts b/apps/status-api/__tests__/controllers/OverallStatusController.test.ts similarity index 100% rename from apps/status-api/__test__/controllers/OverallStatusController.test.ts rename to apps/status-api/__tests__/controllers/OverallStatusController.test.ts diff --git a/apps/status-api/src/controllers/OracleStatusController.ts b/apps/status-api/src/controllers/OracleStatusController.ts new file mode 100644 index 0000000000..cd47cdf6d4 --- /dev/null +++ b/apps/status-api/src/controllers/OracleStatusController.ts @@ -0,0 +1,51 @@ +import { Controller, Get, Param } from '@nestjs/common' +import { WhaleApiClient } from '@defichain/whale-api-client' +import { OraclePriceFeed } from '@defichain/whale-api-client/dist/api/Oracles' +import { SemaphoreCache } from '../../../whale/src/module.api/cache/semaphore.cache' + +type OracleStatus = 'outage' | 'operational' + +@Controller('oracles') +export class OracleStatusController { + constructor ( + private readonly client: WhaleApiClient, + protected readonly cache: SemaphoreCache + ) { + } + + /** + * To provide the status of each oracle given the address based on the last published time for any given token + * + * @param oracleAddress + * @return {Promise} + */ + @Get('/:address') + async getOracleStatus ( + @Param('address') oracleAddress: string + ): Promise<{ status: OracleStatus }> { + const oraclePriceFeed: OraclePriceFeed = await this.cachedGet(`oracle-${oracleAddress}`, async () => { + const oracle = await this.client.oracles.getOracleByAddress(oracleAddress) + return (await this.client.oracles.getPriceFeed(oracle.id, oracle.priceFeeds[0].token, oracle.priceFeeds[0].currency, 1))[0] + }, 5000) + + const nowEpoch = Date.now() + const latestPublishedTime = oraclePriceFeed.block.medianTime * 1000 + const timeDiff = nowEpoch - latestPublishedTime + + return { + status: timeDiff <= (45 * 60 * 1000) ? 'operational' : 'outage' + } + } + + private async cachedGet (field: string, fetch: () => Promise, ttl: number): Promise { + const object = await this.cache.get(`OracleStatusController.${field}`, fetch, { ttl }) + return requireValue(object, field) + } +} + +function requireValue (value: T | undefined, name: string): T { + if (value === undefined) { + throw new Error(`failed to compute: ${name}`) + } + return value +} diff --git a/apps/status-api/src/modules/ControllerModule.ts b/apps/status-api/src/modules/ControllerModule.ts index 8f285a690d..9457512356 100644 --- a/apps/status-api/src/modules/ControllerModule.ts +++ b/apps/status-api/src/modules/ControllerModule.ts @@ -3,6 +3,8 @@ import { ActuatorController } from '@defichain-apps/libs/actuator' import { BlockchainStatusController } from '../controllers/BlockchainStatusController' import { WhaleApiClient } from '@defichain/whale-api-client' import { ConfigService } from '@nestjs/config' +import { SemaphoreCache } from '../../../whale/src/module.api/cache/semaphore.cache' +import { OracleStatusController } from '../controllers/OracleStatusController' import { OverallStatusController } from '../controllers/OverallStatusController' /** @@ -13,8 +15,10 @@ import { OverallStatusController } from '../controllers/OverallStatusController' CacheModule.register() ], controllers: [ + ActuatorController, BlockchainStatusController, ActuatorController, + OracleStatusController, OverallStatusController ], providers: [ @@ -29,7 +33,8 @@ import { OverallStatusController } from '../controllers/OverallStatusController' }) }, inject: [ConfigService] - } + }, + SemaphoreCache ] }) export class ControllerModule { diff --git a/apps/whale/src/module.api/oracle.controller.ts b/apps/whale/src/module.api/oracle.controller.ts index 18ad8f56be..98cb7f6dca 100644 --- a/apps/whale/src/module.api/oracle.controller.ts +++ b/apps/whale/src/module.api/oracle.controller.ts @@ -1,8 +1,8 @@ import { Controller, Get, NotFoundException, Param, Query } from '@nestjs/common' import { Oracle, OracleMapper } from '../module.model/oracle' import { OraclePriceFeed, OraclePriceFeedMapper } from '../module.model/oracle.price.feed' -import { ApiPagedResponse } from '../module.api/_core/api.paged.response' -import { PaginationQuery } from '../module.api/_core/api.query' +import { ApiPagedResponse } from './_core/api.paged.response' +import { PaginationQuery } from './_core/api.query' @Controller('/oracles') export class OracleController {