Skip to content
This repository has been archived by the owner on Jun 3, 2022. It is now read-only.

feat(indexer): Indexing Oracles Data #853

Merged
merged 9 commits into from
Mar 16, 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
129 changes: 129 additions & 0 deletions packages/whale-api-client/__tests__/api/prices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { StubService } from '../stub.service'
import { WhaleApiClient } from '../../src'
import { StubWhaleApiClient } from '../stub.client'
import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc'
import { PriceFeedTimeInterval } from '@whale-api-client/api/prices'
import { Testing } from '@defichain/jellyfish-testing'
import { OracleIntervalSeconds } from '../../../../src/module.model/oracle.price.aggregated.interval'

describe('oracles', () => {
let container: MasterNodeRegTestContainer
Expand Down Expand Up @@ -238,6 +240,133 @@ describe('oracles', () => {
})
})

describe('pricefeed with interval', () => {
const container = new MasterNodeRegTestContainer()
const service = new StubService(container)
const apiClient = new StubWhaleApiClient(service)
let client: JsonRpcClient

beforeAll(async () => {
await container.start()
await container.waitForWalletCoinbaseMaturity()
await service.start()

client = new JsonRpcClient(await container.getCachedRpcUrl())
})

afterAll(async () => {
try {
await service.stop()
} finally {
await container.stop()
}
})

it('should get interval', async () => {
const address = await container.getNewAddress()
const oracleId = await client.oracle.appointOracle(address, [
{ token: 'S1', currency: 'USD' }
], {
weightage: 1
})
await container.generate(1)

const oneMinute = 60
let price = 0
let mockTime = Math.floor(new Date().getTime() / 1000)
for (let h = 0; h < 24; h++) { // loop for 24 hours to make a day
for (let z = 0; z < 4; z++) { // loop for 4 x 15 mins interval to make an hour
mockTime += (15 * oneMinute) + 1 // +1 sec to fall into the next 15 mins bucket
await client.misc.setMockTime(mockTime)
await container.generate(2)

await client.oracle.setOracleData(oracleId, mockTime, {
prices: [
{
tokenAmount: `${(++price).toFixed(2)}@S1`,
currency: 'USD'
}
]
})
await container.generate(1)
}
}

const height = await container.getBlockCount()
await container.generate(1)
await service.waitForIndexedHeight(height)

const noInterval = await apiClient.prices.getFeed('S1', 'USD', height)
expect(noInterval.length).toStrictEqual(96)

const interval15Mins = await apiClient.prices.getFeedWithInterval('S1', 'USD', PriceFeedTimeInterval.FIFTEEN_MINUTES, height)
expect(interval15Mins.length).toStrictEqual(96)
let prevMedianTime = 0
let checkPrice = price
interval15Mins.forEach(value => {
expect(value.aggregated.amount).toStrictEqual(checkPrice.toFixed(8)) // check if price is descending in intervals of 1
checkPrice--
if (prevMedianTime !== 0) { // check if time interval is in 15 mins block
expect(prevMedianTime - value.block.medianTime - 1).toStrictEqual(OracleIntervalSeconds.FIFTEEN_MINUTES) // account for +1 in mock time
}
prevMedianTime = value.block.medianTime
})

const interval1Hour = await apiClient.prices.getFeedWithInterval('S1', 'USD', PriceFeedTimeInterval.ONE_HOUR, height)
expect(interval1Hour.length).toStrictEqual(24)
prevMedianTime = 0
interval1Hour.forEach(value => { // check if time interval is in 1-hour block
if (prevMedianTime !== 0) {
expect(prevMedianTime - value.block.medianTime - 4).toStrictEqual(OracleIntervalSeconds.ONE_HOUR) // account for + 1 per block in mock time
}
prevMedianTime = value.block.medianTime
})
expect(interval1Hour.map(x => x.aggregated.amount)).toStrictEqual(
[
'94.50000000',
'90.50000000',
'86.50000000',
'82.50000000',
'78.50000000',
'74.50000000',
'70.50000000',
'66.50000000',
'62.50000000',
'58.50000000',
'54.50000000',
'50.50000000',
'46.50000000',
'42.50000000',
'38.50000000',
'34.50000000',
'30.50000000',
'26.50000000',
'22.50000000',
'18.50000000',
'14.50000000',
'10.50000000',
'6.50000000',
'2.50000000'
]
)

const interval1Day = await apiClient.prices.getFeedWithInterval('S1', 'USD', PriceFeedTimeInterval.ONE_DAY, height)
expect(interval1Day.length).toStrictEqual(1)
prevMedianTime = 0
interval1Day.forEach(value => { // check if time interval is in 1-day block
if (prevMedianTime !== 0) {
expect(prevMedianTime - value.block.medianTime - 96).toStrictEqual(OracleIntervalSeconds.ONE_DAY) // account for + 1 per block in mock time
}
prevMedianTime = value.block.medianTime
})
expect(interval1Day.map(x => x.aggregated.amount)).toStrictEqual(
[
'48.50000000'
]
)
})
})

describe('active price', () => {
const container = new MasterNodeRegTestContainer()
const testing = Testing.create(container)
Expand Down
40 changes: 40 additions & 0 deletions packages/whale-api-client/src/api/prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import { WhaleApiClient } from '../whale.api.client'
import { ApiPagedResponse } from '../whale.api.response'
import { OraclePriceFeed } from './oracles'

/**
* Time interval for graphing
*/
export enum PriceFeedTimeInterval {
FIFTEEN_MINUTES = 15 * 60,
ONE_HOUR = 60 * 60,
ONE_DAY = 24 * 60 * 60
}

/**
* DeFi whale endpoint for price related services.
*/
Expand Down Expand Up @@ -51,6 +60,11 @@ export class Prices {
return await this.client.requestList('GET', `prices/${key}/feed`, size, next)
}

async getFeedWithInterval (token: string, currency: string, interval: PriceFeedTimeInterval, size: number = 30, next?: string): Promise<ApiPagedResponse<PriceFeedInterval>> {
const key = `${token}-${currency}`
return await this.client.requestList('GET', `prices/${key}/feed/interval/${interval}`, size, next)
}

/**
* Get a list of Oracles
*
Expand Down Expand Up @@ -97,6 +111,32 @@ export interface PriceFeed {
}
}

export interface PriceFeedInterval {
id: string
key: string
sort: string

token: string
currency: string

aggregated: {
amount: string
weightage: number
count: number
oracles: {
active: number
total: number
}
}

block: {
hash: string
height: number
time: number
medianTime: number
}
}

export interface PriceOracle {
id: string
key: string
Expand Down
28 changes: 9 additions & 19 deletions src/module.api/price.controller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Controller, Get, Param, ParseIntPipe, Query } from '@nestjs/common'
import { OraclePriceAggregated, OraclePriceAggregatedMapper } from '@src/module.model/oracle.price.aggregated'
import { OracleTokenCurrencyMapper } from '@src/module.model/oracle.token.currency'
import { OraclePriceAggregatedInterval, OraclePriceAggregatedIntervalMapper } from '@src/module.model/oracle.price.aggregated.interval'
import { ApiPagedResponse } from '@src/module.api/_core/api.paged.response'
import { PaginationQuery } from '@src/module.api/_core/api.query'
import { PriceTicker, PriceTickerMapper } from '@src/module.model/price.ticker'
import { PriceOracle } from '@whale-api-client/api/prices'
import { OraclePriceFeedMapper } from '@src/module.model/oracle.price.feed'
import { OraclePriceActive, OraclePriceActiveMapper } from '@src/module.model/oracle.price.active'
import { ApiError } from '@src/module.api/_core/api.error'

@Controller('/prices')
export class PriceController {
Expand All @@ -16,7 +16,8 @@ export class PriceController {
protected readonly oracleTokenCurrencyMapper: OracleTokenCurrencyMapper,
protected readonly priceTickerMapper: PriceTickerMapper,
protected readonly priceFeedMapper: OraclePriceFeedMapper,
protected readonly oraclePriceActiveMapper: OraclePriceActiveMapper
protected readonly oraclePriceActiveMapper: OraclePriceActiveMapper,
protected readonly oraclePriceAggregatedIntervalMapper: OraclePriceAggregatedIntervalMapper
) {
}

Expand Down Expand Up @@ -64,8 +65,12 @@ export class PriceController {
@Param('key') key: string,
@Param('interval', ParseIntPipe) interval: number,
@Query() query: PaginationQuery
): Promise<ApiPagedResponse<any>> {
return new DeprecatedIntervalApiPagedResponse()
): Promise<ApiPagedResponse<OraclePriceAggregatedInterval>> {
const priceKey = `${key}-${interval}`
const items = await this.oraclePriceAggregatedIntervalMapper.query(priceKey, query.size, query.next)
return ApiPagedResponse.of(items, query.size, item => {
return item.sort
})
}

@Get('/:key/oracles')
Expand All @@ -86,18 +91,3 @@ export class PriceController {
})
}
}

class DeprecatedIntervalApiPagedResponse<T> extends ApiPagedResponse<T> {
error?: ApiError

constructor () {
super([])
this.error = {
at: Date.now(),
code: 410,
type: 'Gone',
message: 'Oracle feed interval data has been deprecated with immediate effect. See https://github.com/DeFiCh/whale/pull/749 for more information.',
url: '/:key/feed/interval/:interval'
}
}
}
3 changes: 3 additions & 0 deletions src/module.indexer/model/dftx.indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppointOracleIndexer } from '@src/module.indexer/model/dftx/appoint.ora
import { RemoveOracleIndexer } from '@src/module.indexer/model/dftx/remove.oracle'
import { UpdateOracleIndexer } from '@src/module.indexer/model/dftx/update.oracle'
import { SetOracleDataIndexer } from '@src/module.indexer/model/dftx/set.oracle.data'
import { SetOracleDataIntervalIndexer } from '@src/module.indexer/model/dftx/set.oracle.data.interval'
import { CreateMasternodeIndexer } from '@src/module.indexer/model/dftx/create.masternode'
import { ResignMasternodeIndexer } from '@src/module.indexer/model/dftx/resign.masternode'
import { Injectable, Logger } from '@nestjs/common'
Expand All @@ -30,6 +31,7 @@ export class MainDfTxIndexer extends Indexer {
removeOracle: RemoveOracleIndexer,
updateOracle: UpdateOracleIndexer,
setOracleData: SetOracleDataIndexer,
setOracleDataInterval: SetOracleDataIntervalIndexer,
createMasternode: CreateMasternodeIndexer,
resignMasternode: ResignMasternodeIndexer,
createToken: CreateTokenIndexer,
Expand All @@ -48,6 +50,7 @@ export class MainDfTxIndexer extends Indexer {
updateOracle,
removeOracle,
setOracleData,
setOracleDataInterval,
createMasternode,
resignMasternode,
createToken,
Expand Down
2 changes: 2 additions & 0 deletions src/module.indexer/model/dftx/_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppointOracleIndexer } from '@src/module.indexer/model/dftx/appoint.ora
import { RemoveOracleIndexer } from '@src/module.indexer/model/dftx/remove.oracle'
import { UpdateOracleIndexer } from '@src/module.indexer/model/dftx/update.oracle'
import { SetOracleDataIndexer } from '@src/module.indexer/model/dftx/set.oracle.data'
import { SetOracleDataIntervalIndexer } from '@src/module.indexer/model/dftx/set.oracle.data.interval'
import { CreateMasternodeIndexer } from '@src/module.indexer/model/dftx/create.masternode'
import { ResignMasternodeIndexer } from '@src/module.indexer/model/dftx/resign.masternode'
import { CreateTokenIndexer } from '@src/module.indexer/model/dftx/create.token'
Expand All @@ -21,6 +22,7 @@ const indexers = [
AppointOracleIndexer,
RemoveOracleIndexer,
SetOracleDataIndexer,
SetOracleDataIntervalIndexer,
UpdateOracleIndexer,
CreateMasternodeIndexer,
ResignMasternodeIndexer,
Expand Down
Loading