Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(status-api): Status API for Oracles #1315

Merged
merged 16 commits into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved
}
}

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'
nichellekoh marked this conversation as resolved.
Show resolved Hide resolved
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