Skip to content

Commit

Permalink
feat(status-api): Status API for Oracles (#1315)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
nichellekoh and joeldavidw authored Apr 20, 2022
1 parent 70ce784 commit 96f768e
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 6 deletions.
23 changes: 20 additions & 3 deletions apps/status-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
@@ -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/<address> - 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/<address> - 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<ApiPagedResponse<OraclePriceFeed>> {
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<Oracle> {
return {
id: '',
block: {
hash: '',
height: 0,
medianTime: 0,
time: 0
},
ownerAddress: '',
priceFeeds: [{
token: '',
currency: ''
}],
weightage: 0
}
}
51 changes: 51 additions & 0 deletions apps/status-api/src/controllers/OracleStatusController.ts
Original file line number Diff line number Diff line change
@@ -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<OracleStatus>}
*/
@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<T> (field: string, fetch: () => Promise<T>, ttl: number): Promise<T> {
const object = await this.cache.get(`OracleStatusController.${field}`, fetch, { ttl })
return requireValue(object, field)
}
}

function requireValue<T> (value: T | undefined, name: string): T {
if (value === undefined) {
throw new Error(`failed to compute: ${name}`)
}
return value
}
7 changes: 6 additions & 1 deletion apps/status-api/src/modules/ControllerModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand All @@ -13,8 +15,10 @@ import { OverallStatusController } from '../controllers/OverallStatusController'
CacheModule.register()
],
controllers: [
ActuatorController,
BlockchainStatusController,
ActuatorController,
OracleStatusController,
OverallStatusController
],
providers: [
Expand All @@ -29,7 +33,8 @@ import { OverallStatusController } from '../controllers/OverallStatusController'
})
},
inject: [ConfigService]
}
},
SemaphoreCache
]
})
export class ControllerModule {
Expand Down
4 changes: 2 additions & 2 deletions apps/whale/src/module.api/oracle.controller.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit 96f768e

Please sign in to comment.