From 1794ff8815cd3e75be2b328e41337a4cc2f976c0 Mon Sep 17 00:00:00 2001 From: Cameron Gilbert Date: Mon, 2 Oct 2023 11:25:44 -0400 Subject: [PATCH] fix(add): oracle --- packages/indexer-nibi/src/defaultObjects.ts | 15 ++++ packages/indexer-nibi/src/gql/generated.ts | 70 ++++++++++++++- packages/indexer-nibi/src/gql/schema.graphql | 59 +++++++++++- .../indexer-nibi/src/heart-monitor.test.ts | 51 +++++++++++ packages/indexer-nibi/src/heart-monitor.ts | 26 ++++++ packages/indexer-nibi/src/query/index.ts | 1 + packages/indexer-nibi/src/query/oracle.ts | 90 +++++++++++++++++++ .../indexer-nibi/src/subscription/index.ts | 1 + .../subscription/oraclePricesSubscription.ts | 36 ++++++++ 9 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 packages/indexer-nibi/src/query/oracle.ts create mode 100644 packages/indexer-nibi/src/subscription/oraclePricesSubscription.ts diff --git a/packages/indexer-nibi/src/defaultObjects.ts b/packages/indexer-nibi/src/defaultObjects.ts index 5776472f..dbff153d 100644 --- a/packages/indexer-nibi/src/defaultObjects.ts +++ b/packages/indexer-nibi/src/defaultObjects.ts @@ -7,6 +7,8 @@ import { GovVote, Governance, MarkPriceCandle, + OracleEntry, + OraclePrice, PerpLeaderboard, PerpMarket, PerpPosition, @@ -340,3 +342,16 @@ export const defaultUnbondings: Unbonding = { delegator: defaultUser, validator: defaultValidator, } + +export const defaultOraclePrice: OraclePrice = { + block: defaultBlock, + eventSeqNo: 0, + pair: "", + price: 0, + txSeqNo: 0, +} + +export const defaultOracleEntry: OracleEntry = { + numVotes: 0, + validator: defaultValidator, +} diff --git a/packages/indexer-nibi/src/gql/generated.ts b/packages/indexer-nibi/src/gql/generated.ts index c2f3933e..e451baa1 100644 --- a/packages/indexer-nibi/src/gql/generated.ts +++ b/packages/indexer-nibi/src/gql/generated.ts @@ -241,6 +241,64 @@ export enum MarkPriceCandlesOrder { PeriodStartTs = "period_start_ts", } +export type Oracle = { + readonly __typename?: "Oracle" + readonly oraclePrices: ReadonlyArray + readonly oracles: ReadonlyArray +} + +export type OracleOraclePricesArgs = { + limit?: InputMaybe + offset?: InputMaybe + order_by?: InputMaybe + order_desc?: InputMaybe + where?: InputMaybe +} + +export type OracleOraclesArgs = { + limit?: InputMaybe + order_by?: InputMaybe + order_desc?: InputMaybe + where?: InputMaybe +} + +export type OracleEntry = { + readonly __typename?: "OracleEntry" + readonly numVotes: Scalars["Int"]["output"] + readonly validator: Validator +} + +export type OraclePrice = { + readonly __typename?: "OraclePrice" + readonly block: Block + readonly eventSeqNo: Scalars["Int"]["output"] + readonly pair: Scalars["String"]["output"] + readonly price: Scalars["Float"]["output"] + readonly txSeqNo: Scalars["Int"]["output"] +} + +export type OraclePricesFilter = { + readonly block?: InputMaybe + readonly blockTs?: InputMaybe + readonly pair?: InputMaybe +} + +export enum OraclePricesOrder { + Pair = "pair", + Price = "price", + Sequence = "sequence", +} + +export type OraclesFilter = { + readonly numVotes?: InputMaybe + readonly validatorAddressEq?: InputMaybe +} + +export enum OraclesOrder { + NumVotes = "num_votes", + ValidatorAddress = "validator_address", +} + export type PeriodFilter = { readonly periodEq?: InputMaybe readonly periodGt?: InputMaybe @@ -337,7 +395,7 @@ export type PerpMarket = { readonly maintenance_margin_ratio: Scalars["Float"]["output"] readonly mark_price: Scalars["Float"]["output"] readonly mark_price_twap: Scalars["Float"]["output"] - readonly max_funding_rate: Scalars["Float"]["output"] + readonly max_funding_rate?: Maybe readonly max_leverage: Scalars["Float"]["output"] readonly pair: Scalars["String"]["output"] readonly partial_liquidation_ratio: Scalars["Float"]["output"] @@ -444,6 +502,7 @@ export type Query = { readonly distributionCommissions: ReadonlyArray readonly governance: Governance readonly markPriceCandles: ReadonlyArray + readonly oracle: Oracle readonly perp: Perp /** @deprecated Moved to perp sub schema */ readonly perpLeaderboard: ReadonlyArray @@ -980,6 +1039,10 @@ export type StringFilter = { readonly like?: InputMaybe } +export type SubOraclePricesFilter = { + readonly pair: Scalars["String"]["input"] +} + export type SubPerpMarketFilter = { readonly pair: Scalars["String"]["input"] } @@ -992,6 +1055,7 @@ export type SubPerpPositionFilter = { export type Subscription = { readonly __typename?: "Subscription" readonly markPriceCandles: ReadonlyArray + readonly oraclePrices: ReadonlyArray readonly perpMarket: PerpMarket readonly perpPositions: ReadonlyArray } @@ -1001,6 +1065,10 @@ export type SubscriptionMarkPriceCandlesArgs = { where?: InputMaybe } +export type SubscriptionOraclePricesArgs = { + where?: InputMaybe +} + export type SubscriptionPerpMarketArgs = { where: SubPerpMarketFilter } diff --git a/packages/indexer-nibi/src/gql/schema.graphql b/packages/indexer-nibi/src/gql/schema.graphql index 284b7ea9..82368c8d 100644 --- a/packages/indexer-nibi/src/gql/schema.graphql +++ b/packages/indexer-nibi/src/gql/schema.graphql @@ -202,6 +202,57 @@ enum MarkPriceCandlesOrder { period_start_ts } +type Oracle { + oraclePrices( + limit: Int + offset: Int + order_by: OraclePricesOrder + order_desc: Boolean + where: OraclePricesFilter + ): [OraclePrice!]! + oracles( + limit: Int + order_by: OraclesOrder + order_desc: Boolean + where: OraclesFilter + ): [OracleEntry!]! +} + +type OracleEntry { + numVotes: Int! + validator: Validator! +} + +type OraclePrice { + block: Block! + eventSeqNo: Int! + pair: String! + price: Float! + txSeqNo: Int! +} + +input OraclePricesFilter { + block: IntFilter + blockTs: TimeFilter + pair: StringFilter +} + +enum OraclePricesOrder { + pair + price + sequence +} + +input OraclesFilter { + numVotes: IntFilter + validatorAddressEq: String +} + +enum OraclesOrder { + num_votes + validator_address +} + input PeriodFilter { periodEq: Int periodGt: Int @@ -279,7 +330,7 @@ type PerpMarket { maintenance_margin_ratio: Float! mark_price: Float! mark_price_twap: Float! - max_funding_rate: Float! + max_funding_rate: Float max_leverage: Float! pair: String! partial_liquidation_ratio: Float! @@ -403,6 +454,7 @@ type Query { order_desc: Boolean where: MarkPriceCandlesFilter ): [MarkPriceCandle!]! + oracle: Oracle! perp: Perp! perpLeaderboard( limit: Int @@ -843,6 +895,10 @@ input StringFilter { like: String } +input SubOraclePricesFilter { + pair: String! +} + input SubPerpMarketFilter { pair: String! } @@ -857,6 +913,7 @@ type Subscription { limit: Int where: MarkPriceCandlesFilter ): [MarkPriceCandle!]! + oraclePrices(where: SubOraclePricesFilter): [OraclePrice!]! perpMarket(where: SubPerpMarketFilter!): PerpMarket! perpPositions(where: SubPerpPositionFilter!): [PerpPosition!]! } diff --git a/packages/indexer-nibi/src/heart-monitor.test.ts b/packages/indexer-nibi/src/heart-monitor.test.ts index 9d82c6e0..3f2bff9b 100644 --- a/packages/indexer-nibi/src/heart-monitor.test.ts +++ b/packages/indexer-nibi/src/heart-monitor.test.ts @@ -3,6 +3,7 @@ import { cleanResponse, gqlEndptFromTmRpc } from "./gql" import { communityPoolQueryString, delegationsQueryString } from "./query" import { defaultMarkPriceCandles, + defaultOraclePrice, defaultPerpMarket, defaultPerpPosition, } from "./defaultObjects" @@ -204,6 +205,56 @@ test("markPriceCandlesSubscription", async () => { } }) +test("oracle", async () => { + const resp = await heartMonitor.oracle({ + oraclePrices: { + limit: 1, + }, + oracles: { + limit: 1, + }, + }) + expect(resp).toHaveProperty("oracle") + + if (resp.oracle) { + const { oracle } = resp + const fields = ["oraclePrices", "oracles"] + fields.forEach((field: string) => { + expect(oracle).toHaveProperty(field) + }) + } +}) + +test("oraclePricesSubscription", async () => { + const hm = { + oraclePricesSubscription: jest.fn().mockResolvedValue({ + next: async () => ({ + value: { + data: { + oraclePrices: [defaultOraclePrice], + }, + }, + }), + }), + } + + const resp = await hm.oraclePricesSubscription({ + where: { pair: "ubtc:unusd" }, + }) + + const event = await resp.next() + + expect(event.value.data).toHaveProperty("oraclePrices") + + if ((event.value.data.oraclePrices.length ?? 0) > 0) { + const [oraclePrices] = event.value.data.oraclePrices ?? [] + const fields = ["block", "eventSeqNo", "pair", "price", "txSeqNo"] + fields.forEach((field: string) => { + expect(oraclePrices).toHaveProperty(field) + }) + } +}) + test("perp", async () => { const resp = await heartMonitor.perp({ leaderboard: { diff --git a/packages/indexer-nibi/src/heart-monitor.ts b/packages/indexer-nibi/src/heart-monitor.ts index 206e7ffb..b90d5ccc 100644 --- a/packages/indexer-nibi/src/heart-monitor.ts +++ b/packages/indexer-nibi/src/heart-monitor.ts @@ -6,6 +6,7 @@ import { DistributionCommission, Governance, MarkPriceCandle, + OraclePrice, PerpMarket, PerpPosition, QueryCommunityPoolArgs, @@ -30,6 +31,7 @@ import { SpotPoolJoined, SpotPoolSwap, SubscriptionMarkPriceCandlesArgs, + SubscriptionOraclePricesArgs, SubscriptionPerpMarketArgs, SubscriptionPerpPositionsArgs, Token, @@ -77,12 +79,18 @@ import { governance, GqlOutMarkPriceCandles, markPriceCandles, + QueryOracleArgs, + OracleFields, + GqlOutOracle, + oracle, } from "./query" import { markPriceCandlesSubscription, GqlOutPerpMarket, perpMarketSubscription, perpPositionsSubscription, + oraclePricesSubscription, + GqlOutOraclePrices, } from "./subscription" import { queryBatchHandler } from "./batchHandlers/queryBatchHandler" @@ -119,6 +127,16 @@ export interface IHeartMonitor { fields?: Partial ) => Promise>> + readonly oracle: ( + args: QueryOracleArgs, + fields?: OracleFields + ) => Promise + + readonly oraclePricesSubscription: ( + args: SubscriptionOraclePricesArgs, + fields?: Partial + ) => Promise>> + readonly perp: ( args: QueryPerpArgs, fields?: PerpFields @@ -244,6 +262,14 @@ export class HeartMonitor implements IHeartMonitor { fields?: Partial ) => markPriceCandlesSubscription(args, this.subscriptionClient, fields) + oracle = async (args: QueryOracleArgs, fields?: OracleFields) => + oracle(args, this.gqlEndpt, fields) + + oraclePricesSubscription = async ( + args: SubscriptionOraclePricesArgs, + fields?: Partial + ) => oraclePricesSubscription(args, this.subscriptionClient, fields) + perp = async (args: QueryPerpArgs, fields?: PerpFields) => perp(args, this.gqlEndpt, fields) diff --git a/packages/indexer-nibi/src/query/index.ts b/packages/indexer-nibi/src/query/index.ts index b6a2f0ea..1aa78ae0 100644 --- a/packages/indexer-nibi/src/query/index.ts +++ b/packages/indexer-nibi/src/query/index.ts @@ -15,3 +15,4 @@ export * from "./stats" export * from "./unbondings" export * from "./users" export * from "./validators" +export * from "./oracle" diff --git a/packages/indexer-nibi/src/query/oracle.ts b/packages/indexer-nibi/src/query/oracle.ts new file mode 100644 index 00000000..3e32ca70 --- /dev/null +++ b/packages/indexer-nibi/src/query/oracle.ts @@ -0,0 +1,90 @@ +import { defaultOracleEntry, defaultOraclePrice } from "../defaultObjects" +import { convertObjectToPropertiesString, doGqlQuery, gqlQuery } from "../gql" +import { + OracleEntry, + OracleOraclePricesArgs, + OracleOraclesArgs, + OraclePrice, + Query, +} from "../gql/generated" + +export type QueryOracleArgs = { + oraclePrices?: OracleOraclePricesArgs + oracles?: OracleOraclesArgs +} + +export interface GqlOutOracle { + oracle?: Query["oracle"] +} + +export type OracleFields = Partial<{ + oraclePrices?: Partial + oracles?: Partial +}> + +export const oracleQueryString = ( + args: QueryOracleArgs, + fields?: OracleFields +) => { + const oracleQuery: string[] = [] + + if (fields) { + if (fields?.oraclePrices) { + oracleQuery.push( + gqlQuery( + "oraclePrices", + args?.oraclePrices ?? {}, + convertObjectToPropertiesString(fields.oraclePrices), + true + ) + ) + } + + if (fields?.oracles) { + oracleQuery.push( + gqlQuery( + "oracles", + args?.oracles ?? {}, + convertObjectToPropertiesString(fields.oracles), + true + ) + ) + } + } else { + oracleQuery.push( + gqlQuery( + "oraclePrices", + args?.oraclePrices ?? {}, + convertObjectToPropertiesString(defaultOraclePrice), + true + ) + ) + + oracleQuery.push( + gqlQuery( + "oracles", + args?.oracles ?? {}, + convertObjectToPropertiesString(defaultOracleEntry), + true + ) + ) + } + + return ` + oracle { + ${oracleQuery.join("\n")} + } + ` +} + +export const oracle = async ( + args: QueryOracleArgs, + endpt: string, + fields?: OracleFields +): Promise => + doGqlQuery( + `{ + ${oracleQueryString(args, fields)} + }`, + endpt + ) diff --git a/packages/indexer-nibi/src/subscription/index.ts b/packages/indexer-nibi/src/subscription/index.ts index 0419516b..589d9b9f 100644 --- a/packages/indexer-nibi/src/subscription/index.ts +++ b/packages/indexer-nibi/src/subscription/index.ts @@ -1,3 +1,4 @@ export * from "./markPriceCandlesSubscription" +export * from "./oraclePricesSubscription" export * from "./perpMarketSubscription" export * from "./perpPositionsSubscription" diff --git a/packages/indexer-nibi/src/subscription/oraclePricesSubscription.ts b/packages/indexer-nibi/src/subscription/oraclePricesSubscription.ts new file mode 100644 index 00000000..ac855baa --- /dev/null +++ b/packages/indexer-nibi/src/subscription/oraclePricesSubscription.ts @@ -0,0 +1,36 @@ +import { Client, ExecutionResult } from "graphql-ws" +import { + SubscriptionOraclePricesArgs, + OraclePrice, + Subscription, +} from "../gql/generated" +import { defaultOraclePrice } from "../defaultObjects" +import { gqlQuery, convertObjectToPropertiesString } from "../gql" + +export interface GqlOutOraclePrices { + oraclePrices?: Subscription["oraclePrices"] +} + +export const oraclePricesSubscriptionQueryString = ( + args: SubscriptionOraclePricesArgs, + fields?: Partial +) => + `subscription { + ${gqlQuery( + "oraclePrices", + args, + fields + ? convertObjectToPropertiesString(fields) + : convertObjectToPropertiesString(defaultOraclePrice), + true + )} + }` + +export const oraclePricesSubscription = async ( + args: SubscriptionOraclePricesArgs, + client: Client, + fields?: Partial +): Promise>> => + client.iterate({ + query: oraclePricesSubscriptionQueryString(args, fields), + })