From de9d497af529fe902142420bbb1276a5f772dcf2 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Tue, 6 Sep 2022 17:28:00 +0800 Subject: [PATCH 1/3] add new optional metadata property to PriceFeed and test --- src/__tests__/PriceFeed.test.ts | 39 +++++++++++++++++++-- src/index.ts | 60 +++++++++++++++++++++++++++++++-- src/schemas/PriceFeed.ts | 37 ++++++++++++++++++++ src/schemas/price_feed.json | 37 ++++++++++++++++---- 4 files changed, 161 insertions(+), 12 deletions(-) diff --git a/src/__tests__/PriceFeed.test.ts b/src/__tests__/PriceFeed.test.ts index a241080..536412a 100644 --- a/src/__tests__/PriceFeed.test.ts +++ b/src/__tests__/PriceFeed.test.ts @@ -1,4 +1,4 @@ -import { Price, PriceFeed, PriceStatus } from "../index"; +import { Price, PriceFeed, PriceFeedMetadata, PriceStatus } from "../index"; beforeAll(() => { jest.useFakeTimers(); @@ -42,8 +42,7 @@ test("Parsing Price Feed works as expected", () => { new Price("1", 4, "10") ); expect(priceFeed.getLatestAvailablePriceWithinDuration(5)).toBeUndefined(); - - expect(priceFeed.toJson()).toStrictEqual(data); + expect(priceFeed.toJson()).toEqual(data); }); test("getCurrentPrice returns undefined if status is not Trading", () => { @@ -99,3 +98,37 @@ test("getLatestAvailablePrice returns prevPrice when status is not Trading", () ); expect(priceFeed.getLatestAvailablePriceWithinDuration(10)).toBeUndefined(); }); + +test("getMetadata returns PriceFeedMetadata as expected", () => { + const data = { + conf: "1", + ema_conf: "2", + ema_price: "3", + expo: 4, + id: "abcdef0123456789", + max_num_publishers: 6, + metadata: new PriceFeedMetadata({ + attestation_time: 7, + emitter_chain: 8, + sequence_number: 9, + }), + num_publishers: 10, + prev_conf: "11", + prev_price: "12", + prev_publish_time: 13, + price: "14", + product_id: "0123456789abcdef", + publish_time: 16, + status: PriceStatus.Unknown, + }; + + const priceFeed = PriceFeed.fromJson(data); + + expect(priceFeed.getMetadata()).toStrictEqual( + new PriceFeedMetadata({ + attestation_time: 7, + emitter_chain: 8, + sequence_number: 9, + }) + ); +}); diff --git a/src/index.ts b/src/index.ts index c635734..ecf0334 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,36 @@ export enum PriceStatus { Unknown = "Unknown", } +/** + * Metadata about the price + * + * Represents metadata of a price feed. + */ +export class PriceFeedMetadata { + /** + * Attestation time of the price + */ + attestation_time: number; + /** + * Chain of the emitter + */ + emitter_chain: number; + /** + * Sequence number of the price + */ + sequence_number: number; + + constructor(metadata: { + attestation_time: number; + emitter_chain: number; + sequence_number: number; + }) { + this.attestation_time = metadata.attestation_time; + this.emitter_chain = metadata.emitter_chain; + this.sequence_number = metadata.sequence_number; + } +} + /** * Pyth Price Feed * @@ -84,6 +114,10 @@ export class PriceFeed { * Maximum number of allowed publishers that can contribute to a price. */ maxNumPublishers: number; + /** + * Metadata about the price + */ + metadata?: PriceFeedMetadata; /** * Number of publishers that made up current aggregate. */ @@ -124,6 +158,7 @@ export class PriceFeed { expo: number; id: HexString; maxNumPublishers: number; + metadata?: PriceFeedMetadata; numPublishers: number; prevConf: string; prevPrice: string; @@ -139,6 +174,7 @@ export class PriceFeed { this.expo = rawValues.expo; this.id = rawValues.id; this.maxNumPublishers = rawValues.maxNumPublishers; + this.metadata = rawValues.metadata; this.numPublishers = rawValues.numPublishers; this.prevConf = rawValues.prevConf; this.prevPrice = rawValues.prevPrice; @@ -158,6 +194,7 @@ export class PriceFeed { expo: jsonFeed.expo, id: jsonFeed.id, maxNumPublishers: jsonFeed.max_num_publishers, + metadata: jsonFeed.metadata, numPublishers: jsonFeed.num_publishers, prevConf: jsonFeed.prev_conf, prevPrice: jsonFeed.prev_price, @@ -177,6 +214,7 @@ export class PriceFeed { expo: this.expo, id: this.id, max_num_publishers: this.maxNumPublishers, + metadata: this.metadata, num_publishers: this.numPublishers, prev_conf: this.prevConf, prev_price: this.prevPrice, @@ -186,6 +224,7 @@ export class PriceFeed { publish_time: this.publishTime, status: this.status, }; + // this is done to avoid sending undefined values to the server return Convert.priceFeedToJson(jsonFeed); } @@ -259,18 +298,33 @@ export class PriceFeed { * @returns a struct containing the latest available price, confidence interval and the exponent for * both numbers, or `undefined` if no price update occurred within `duration` seconds of the current time. */ - getLatestAvailablePriceWithinDuration(duration: DurationInSeconds): Price | undefined { + getLatestAvailablePriceWithinDuration( + duration: DurationInSeconds + ): Price | undefined { const [price, timestamp] = this.getLatestAvailablePriceUnchecked(); const currentTime: UnixTimestamp = Math.floor(Date.now() / 1000); - + // This checks the absolute difference as a sanity check // for the cases that the system time is behind or price - // feed timestamp happen to be in the future (a bug). + // feed timestamp happen to be in the future (a bug). if (Math.abs(currentTime - timestamp) > duration) { return undefined; } return price; } + + /** + * Get the price feed metadata. + * + * @returns a struct containing the attestation time, emitter chain, and the sequence number. + * Returns `undefined` if metadata is currently unavailable. + */ + getMetadata(): PriceFeedMetadata | undefined { + if (this.metadata === undefined) { + return undefined; + } + return new PriceFeedMetadata(this.metadata); + } } diff --git a/src/schemas/PriceFeed.ts b/src/schemas/PriceFeed.ts index 10741b6..32085cc 100644 --- a/src/schemas/PriceFeed.ts +++ b/src/schemas/PriceFeed.ts @@ -35,6 +35,10 @@ export interface PriceFeed { * Maximum number of allowed publishers that can contribute to a price. */ max_num_publishers: number; + /** + * Metadata about the price + */ + metadata?: PriceFeedMetadata; /** * Number of publishers that made up current aggregate. */ @@ -69,6 +73,26 @@ export interface PriceFeed { status: PriceStatus; } +/** + * Metadata about the price + * + * Represents metadata of a price feed. + */ +export interface PriceFeedMetadata { + /** + * Attestation time of the price + */ + attestation_time: number; + /** + * Chain of the emitter + */ + emitter_chain: number; + /** + * Sequence number of the price + */ + sequence_number: number; +} + /** * Status of price (Trading is valid). * @@ -249,6 +273,11 @@ const typeMap: any = { { json: "expo", js: "expo", typ: 0 }, { json: "id", js: "id", typ: "" }, { json: "max_num_publishers", js: "max_num_publishers", typ: 0 }, + { + json: "metadata", + js: "metadata", + typ: u(undefined, r("PriceFeedMetadata")), + }, { json: "num_publishers", js: "num_publishers", typ: 0 }, { json: "prev_conf", js: "prev_conf", typ: "" }, { json: "prev_price", js: "prev_price", typ: "" }, @@ -260,5 +289,13 @@ const typeMap: any = { ], "any" ), + PriceFeedMetadata: o( + [ + { json: "attestation_time", js: "attestation_time", typ: 0 }, + { json: "emitter_chain", js: "emitter_chain", typ: 0 }, + { json: "sequence_number", js: "sequence_number", typ: 0 }, + ], + "any" + ), PriceStatus: ["Auction", "Halted", "Trading", "Unknown"], }; diff --git a/src/schemas/price_feed.json b/src/schemas/price_feed.json index c73baea..87576ba 100644 --- a/src/schemas/price_feed.json +++ b/src/schemas/price_feed.json @@ -94,6 +94,14 @@ "$ref": "#/definitions/PriceStatus" } ] + }, + "metadata": { + "description": "Metadata about the price", + "allOf": [ + { + "$ref": "#/definitions/PriceFeedMetadata" + } + ] } }, "definitions": { @@ -103,12 +111,29 @@ "PriceStatus": { "description": "Represents availability status of a price feed.", "type": "string", - "enum": [ - "Unknown", - "Trading", - "Halted", - "Auction" - ] + "enum": ["Unknown", "Trading", "Halted", "Auction"] + }, + "PriceFeedMetadata": { + "description": "Represents metadata of a price feed.", + "type": "object", + "required": ["attestation_time", "emitter_chain", "sequence_number"], + "properties": { + "attestation_time": { + "description": "Attestation time of the price", + "type": "integer", + "format": "int64" + }, + "emitter_chain": { + "description": "Chain of the emitter", + "type": "integer", + "format": "int16" + }, + "sequence_number": { + "description": "Sequence number of the price", + "type": "integer", + "format": "int64" + } + } } } } From e32f44a4f67e6c58a1e2b21cf71936382851ee4e Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Tue, 6 Sep 2022 17:28:19 +0800 Subject: [PATCH 2/3] bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7046c4f..dce4508 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-sdk-js", - "version": "0.2.0", + "version": "0.3.0", "description": "Pyth Network SDK in JS", "homepage": "https://pyth.network", "main": "lib/index.js", From 5388085bd7010c99bb4232abf1ae6fcab15fe616 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Tue, 6 Sep 2022 19:05:46 +0800 Subject: [PATCH 3/3] refactor --- package.json | 2 +- src/__tests__/PriceFeed.test.ts | 6 ++-- src/index.ts | 58 +++++++++++++++++++++++---------- src/schemas/PriceFeed.ts | 8 +++++ 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index dce4508..6d582f7 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test": "jest", "build": "tsc", "format": "prettier --write \"src/**/*.ts\"", - "gen-ts-schema": "quicktype --src-lang schema src/schemas/price_feed.json -o src/schemas/PriceFeed.ts --raw-type any && prettier --write \"src/schemas/*.ts\"", + "gen-ts-schema": "quicktype --src-lang schema src/schemas/price_feed.json -o src/schemas/PriceFeed.ts --raw-type any --converters all-objects && prettier --write \"src/schemas/*.ts\"", "lint": "eslint src/", "prepare": "npm run build", "prepublishOnly": "npm test && npm run lint", diff --git a/src/__tests__/PriceFeed.test.ts b/src/__tests__/PriceFeed.test.ts index 536412a..b599e66 100644 --- a/src/__tests__/PriceFeed.test.ts +++ b/src/__tests__/PriceFeed.test.ts @@ -107,11 +107,11 @@ test("getMetadata returns PriceFeedMetadata as expected", () => { expo: 4, id: "abcdef0123456789", max_num_publishers: 6, - metadata: new PriceFeedMetadata({ + metadata: { attestation_time: 7, emitter_chain: 8, sequence_number: 9, - }), + }, num_publishers: 10, prev_conf: "11", prev_price: "12", @@ -125,7 +125,7 @@ test("getMetadata returns PriceFeedMetadata as expected", () => { const priceFeed = PriceFeed.fromJson(data); expect(priceFeed.getMetadata()).toStrictEqual( - new PriceFeedMetadata({ + PriceFeedMetadata.fromJson({ attestation_time: 7, emitter_chain: 8, sequence_number: 9, diff --git a/src/index.ts b/src/index.ts index ecf0334..d549b92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,8 @@ -import { Convert, PriceFeed as JsonPriceFeed } from "./schemas/PriceFeed"; +import { + Convert, + PriceFeed as JsonPriceFeed, + PriceFeedMetadata as JsonPriceFeedMetadata, +} from "./schemas/PriceFeed"; export type UnixTimestamp = number; export type DurationInSeconds = number; @@ -62,24 +66,46 @@ export class PriceFeedMetadata { /** * Attestation time of the price */ - attestation_time: number; + attestationTime: number; /** * Chain of the emitter */ - emitter_chain: number; + emitterChain: number; /** * Sequence number of the price */ - sequence_number: number; + sequenceNumber: number; constructor(metadata: { - attestation_time: number; - emitter_chain: number; - sequence_number: number; + attestationTime: number; + emitterChain: number; + sequenceNumber: number; }) { - this.attestation_time = metadata.attestation_time; - this.emitter_chain = metadata.emitter_chain; - this.sequence_number = metadata.sequence_number; + this.attestationTime = metadata.attestationTime; + this.emitterChain = metadata.emitterChain; + this.sequenceNumber = metadata.sequenceNumber; + } + + static fromJson(json: any): PriceFeedMetadata | undefined { + if (json === undefined) { + return undefined; + } + const jsonFeed: JsonPriceFeedMetadata = Convert.toPriceFeedMetadata(json); + return new PriceFeedMetadata({ + attestationTime: jsonFeed.attestation_time, + emitterChain: jsonFeed.emitter_chain, + sequenceNumber: jsonFeed.sequence_number, + }); + } + + toJson(): any { + const jsonFeed: JsonPriceFeedMetadata = { + attestation_time: this.attestationTime, + emitter_chain: this.emitterChain, + sequence_number: this.sequenceNumber, + }; + // this is done to avoid sending undefined values to the server + return Convert.priceFeedMetadataToJson(jsonFeed); } } @@ -194,7 +220,7 @@ export class PriceFeed { expo: jsonFeed.expo, id: jsonFeed.id, maxNumPublishers: jsonFeed.max_num_publishers, - metadata: jsonFeed.metadata, + metadata: PriceFeedMetadata.fromJson(jsonFeed.metadata), numPublishers: jsonFeed.num_publishers, prevConf: jsonFeed.prev_conf, prevPrice: jsonFeed.prev_price, @@ -214,7 +240,7 @@ export class PriceFeed { expo: this.expo, id: this.id, max_num_publishers: this.maxNumPublishers, - metadata: this.metadata, + metadata: this.metadata?.toJson(), num_publishers: this.numPublishers, prev_conf: this.prevConf, prev_price: this.prevPrice, @@ -224,7 +250,6 @@ export class PriceFeed { publish_time: this.publishTime, status: this.status, }; - // this is done to avoid sending undefined values to the server return Convert.priceFeedToJson(jsonFeed); } @@ -318,13 +343,10 @@ export class PriceFeed { /** * Get the price feed metadata. * - * @returns a struct containing the attestation time, emitter chain, and the sequence number. + * @returns a struct containing the attestation time, emitter chain, and the sequence number. * Returns `undefined` if metadata is currently unavailable. */ getMetadata(): PriceFeedMetadata | undefined { - if (this.metadata === undefined) { - return undefined; - } - return new PriceFeedMetadata(this.metadata); + return this.metadata; } } diff --git a/src/schemas/PriceFeed.ts b/src/schemas/PriceFeed.ts index 32085cc..40b61e2 100644 --- a/src/schemas/PriceFeed.ts +++ b/src/schemas/PriceFeed.ts @@ -115,6 +115,14 @@ export class Convert { public static priceFeedToJson(value: PriceFeed): any { return uncast(value, r("PriceFeed")); } + + public static toPriceFeedMetadata(json: any): PriceFeedMetadata { + return cast(json, r("PriceFeedMetadata")); + } + + public static priceFeedMetadataToJson(value: PriceFeedMetadata): any { + return uncast(value, r("PriceFeedMetadata")); + } } function invalidValue(typ: any, val: any, key: any = ""): never {