@d8x/perpetuals-sdk
Version:
Node TypeScript SDK for D8X Perpetual Futures
778 lines (742 loc) • 30.4 kB
text/typescript
import { Buffer } from "buffer";
import { decNToFloat } from "./d8XMath";
import type {
IdxPriceInfo,
PredMktPriceInfo,
PriceFeedConfig,
PriceFeedEndpoints,
PriceFeedEndpointsOptionalWrite,
PriceFeedFormat,
PriceFeedSubmission,
PythV2LatestPriceFeed,
} from "./nodeSDKTypes";
import OnChainPxFactory from "./onChainPxFactory";
import OnChainPxFeed from "./onChainPxFeed";
import PerpetualDataHandler from "./perpetualDataHandler";
import PolyMktsPxFeed from "./polyMktsPxFeed";
import Triangulator from "./triangulator";
import { sleepForSec } from "./utils";
/**
* This class communicates with the REST API that provides price-data that is
* to be submitted to the smart contracts for certain functions such as
* trader liquidations, trade executions, change of trader margin amount.
*/
export default class PriceFeeds {
private config: PriceFeedConfig | undefined;
private priceFeedConfigNetwork: string;
// Read only price info endpoints. Used by default. feedEndpoints[endpointId]
// = endpointstring
public feedEndpoints: Array<string> = [];
// Endpoints which are used to fetch prices for submissions
public writeFeedEndpoints: Array<string> = [];
private feedInfo: Map<string, { symbol: string; endpointId: number }[]>; // priceFeedId -> [symbol, endpointId]
private dataHandler: PerpetualDataHandler;
// store triangulation paths given the price feeds
private triangulations: Map<string, [string[], boolean[]]>;
private THRESHOLD_MARKET_CLOSED_SEC = 15; // smallest lag for which we consider the market as being closed
private cache: Map<string, { timestamp: number; values: any }> = new Map();
private onChainPxFeeds: Map<string, OnChainPxFeed>;
// api formatting constants
private PYTH = { endpoint: "/v2/updates/price/latest?encoding=base64&ids[]=", separator: "&ids[]=", suffix: "" };
private polyMktsPxFeed: PolyMktsPxFeed | undefined;
constructor(dataHandler: PerpetualDataHandler, priceFeedConfigNetwork: string) {
this.priceFeedConfigNetwork = priceFeedConfigNetwork;
this.onChainPxFeeds = new Map<string, OnChainPxFeed>();
this.dataHandler = dataHandler;
this.triangulations = new Map<string, [string[], boolean[]]>();
this.feedInfo = new Map<string, { symbol: string; endpointId: number }[]>();
}
/**
* initialization function. Gathers config from config-hub if url
* specified
*/
public async init() {
let configs: PriceFeedConfig[];
const configSrc = this.dataHandler.config.configSource;
if (configSrc == "" || configSrc == undefined) {
// embedded config
configs = require("./config/priceFeedConfig.json") as PriceFeedConfig[];
} else {
//load remote config
const res = await fetch(configSrc + "/priceFeedConfig.json");
if (res.status !== 200) {
throw new Error(`failed to fetch priceFeedConfig status code: ${res.status}`);
}
if (!res.ok) {
throw new Error(`failed to fetch config (${res.status}): ${res.statusText} ${configSrc}`);
}
configs = await res.json();
// console.log("fetched price config from source", configSrc + "/priceFeedConfig.json");
}
this.config = PriceFeeds._selectConfig(configs, this.priceFeedConfigNetwork);
// if SDK config contains custom price feed endpoints, these override the
// public/default ones
if (this.dataHandler.config.priceFeedEndpoints && this.dataHandler.config.priceFeedEndpoints.length > 0) {
this.config.endpoints = PriceFeeds.overridePriceEndpointsOfSameType(
this.config.endpoints,
this.dataHandler.config.priceFeedEndpoints
);
}
for (let k = 0; k < this.config.ids.length; k++) {
if (this.config.ids[k].type == "onchain") {
let sym = this.config.ids[k].symbol;
this.onChainPxFeeds.set(sym, OnChainPxFactory.createFeed(sym)!);
}
}
[this.feedInfo, this.feedEndpoints, this.writeFeedEndpoints] = PriceFeeds._constructFeedInfo(this.config, false);
// Deny providing no endpoints
if (this.feedEndpoints.length == 0) {
throw new Error("PriceFeeds: no endpoints provided in config");
}
if (this.writeFeedEndpoints.length == 0) {
throw new Error("PriceFeeds: no writeEndpoints provided in config");
}
this.polyMktsPxFeed = new PolyMktsPxFeed(this.config);
}
public getConfig(): PriceFeedConfig {
if (this.config == undefined) {
throw Error("init() required");
}
return this.config;
}
// overridePriceEndpointsOfSameType overrides endpoints of config with same
// type endpoints provided by user and returns the updated price feed
// endpoints list.
public static overridePriceEndpointsOfSameType(
configEndpoints: PriceFeedEndpoints,
userProvidedEndpoints: PriceFeedEndpointsOptionalWrite
): PriceFeedEndpoints {
let result = configEndpoints;
for (let k = 0; k < userProvidedEndpoints.length; k++) {
for (let j = 0; j < result.length; j++) {
if (result[j].type == userProvidedEndpoints[k].type) {
// read only endpoints
if (userProvidedEndpoints[k].endpoints.length > 0) {
result[j].endpoints = userProvidedEndpoints[k].endpoints;
for (let i = 0; i < result[j].endpoints.length; i++) {
result[j].endpoints[i] = PriceFeeds.trimEndpoint(result[j].endpoints[i]);
}
}
// write endpoints
if (
userProvidedEndpoints[k].writeEndpoints !== undefined &&
userProvidedEndpoints[k].writeEndpoints!.length > 0
) {
result[j].writeEndpoints = userProvidedEndpoints[k].writeEndpoints!;
for (let i = 0; i < result[j].writeEndpoints.length; i++) {
result[j].writeEndpoints[i] = PriceFeeds.trimEndpoint(result[j].writeEndpoints[i]);
}
}
break;
}
}
}
return result;
}
/**
* We cut last / or legacy url format /api/ if any
* @param endp endpoint string
* @returns trimmed string
*/
public static trimEndpoint(endp: string): string {
// cut last /
let regex = new RegExp("/$");
endp = endp.replace(regex, "");
regex = new RegExp("/api$");
endp = endp.replace(regex, "");
return endp;
}
/**
* Pre-processing of triangulations for symbols, given the price feeds
* @param symbols set of symbols we want to triangulate from price feeds
*/
public initializeTriangulations(symbols: Set<string>) {
let feedSymbols = new Array<string>();
for (let [, value] of this.feedInfo) {
for (let j = 0; j < value.length; j++) {
feedSymbols.push(value[j].symbol);
}
}
for (let symbol of symbols.values()) {
let triangulation = Triangulator.triangulate(feedSymbols, symbol);
this.triangulations.set(symbol, triangulation);
}
}
/**
* Returns computed triangulation map
* @returns Triangulation map
*/
public getTriangulations() {
return this.triangulations;
}
/**
* Set pre-computed triangulation map
*/
public setTriangulations(triangulation: Map<string, [string[], boolean[]]>) {
this.triangulations = triangulation;
}
/**
* Get required information to be able to submit a blockchain transaction with
* price-update such as trade execution, liquidation. Uses write price feed endpoints.
* @param symbol symbol of perpetual, e.g., BTC-USD-MATIC
* @returns PriceFeedSubmission, index prices, market closed information
*/
public async fetchFeedPriceInfoAndIndicesForPerpetual(
symbol: string
): Promise<{ submission: PriceFeedSubmission; pxS2S3: [number, number]; mktClosed: [boolean, boolean] }> {
let indexSymbols = await this.dataHandler.getIndexSymbols(symbol);
// fetch prices from required price-feeds (REST)
let submission: PriceFeedSubmission = await this.fetchLatestFeedPriceInfoForPerpetual(symbol);
// calculate index-prices from price-feeds
let [_idxPrices, _mktClosed] = this.calculateTriangulatedPricesFromFeedInfo(
indexSymbols.filter((x) => x != ""),
submission
);
let idxPrices: [number, number] = [_idxPrices[0], 0];
let mktClosed: [boolean, boolean] = [_mktClosed[0], false];
if (idxPrices.length > 1) {
idxPrices[1] = _idxPrices[1];
mktClosed[1] = _mktClosed[1];
}
return { submission: submission, pxS2S3: idxPrices, mktClosed: mktClosed };
}
/**
* Get all prices/isMarketClosed for the provided symbols via
* "latest_price_feeds" and triangulation. Triangulation must be defined in
* config, unless it is a direct price feed. Uses read endpoints.
* @param symbol perpetual symbol of the form BTC-USD-MATIC
* @returns map of feed-price symbol to price/isMarketClosed
*/
public async fetchPrices(symbols: string[]): Promise<Map<string, [number, boolean]>> {
let feedPrices = await this.fetchAllFeedPrices();
let [prices, mktClosed] = this.triangulatePricesFromFeedPrices(symbols, feedPrices);
let symMap = new Map<string, [number, boolean]>();
for (let k = 0; k < symbols.length; k++) {
symMap.set(symbols[k], [prices[k], mktClosed[k]]);
}
// set emas
for (const key of feedPrices.keys()) {
if (!key.includes(":ema")) {
continue;
}
// only report ema if corresponding symbol is requested
const keyNoEma = key.substring(0, key.length - 5);
if (!symbols.includes(keyNoEma)) {
continue;
}
let p = feedPrices.get(key);
symMap.set(key, p!);
}
return symMap;
}
/**
* Get index prices and market closed information for the given perpetual
* @param symbol perpetual symbol such as ETH-USD-MATIC
* @returns Index prices and market closed information; for prediction markets also
* ema, confidence, and order book parameters.
*/
public async fetchPricesForPerpetual(symbol: string): Promise<IdxPriceInfo> {
if (this.polyMktsPxFeed == undefined) {
throw Error("init() required");
}
let indexSymbols = (await this.dataHandler.getIndexSymbols(symbol)).filter((x) => x != "");
if (this.dataHandler.isPredictionMarket(symbol)) {
let priceObj = (await this.polyMktsPxFeed.fetchPricesForSyms([indexSymbols[0]]))[0];
const s3map = await this.fetchFeedPrices([indexSymbols[1]]);
const s3 = s3map.get(indexSymbols[1])!;
const pxInfo: IdxPriceInfo = {
s2: priceObj.s2,
s3: s3[0],
ema: priceObj.ema,
s2MktClosed: priceObj.s2MktClosed,
s3MktClosed: s3[1],
conf: priceObj.conf,
predMktCLOBParams: priceObj.predMktCLOBParams,
};
return pxInfo;
}
// determine relevant price feeds
let feedSymbols = new Array<string>();
for (let sym of indexSymbols) {
if (sym != "") {
let triang: [string[], boolean[]] | undefined = this.triangulations.get(sym);
if (triang == undefined) {
// no triangulation defined, so symbol must be a feed (unless misconfigured)
feedSymbols.push(sym);
} else {
// push all required feeds to array
triang[0].map((feedSym) => feedSymbols.push(feedSym));
}
}
}
// get all feed prices
const feedResponses = await this.fetchFeedPriceResponses(feedSymbols);
// triangulate
let feedPrices = new Map();
for (const [key, value] of feedResponses) {
feedPrices.set(key, [value[0], value[1]]);
}
let [prices, mktClosed] = this.triangulatePricesFromFeedPrices(indexSymbols, feedPrices);
// if low liquidity, assume base-quote is a feed
if (this.dataHandler.isLowLiquidityMarket(symbol)) {
const feedSymbol = feedSymbols.find((fs) => fs.split("-")[0] === indexSymbols[0].split("-")[0])!;
const [, , s2Info, s2EmaInfo] = feedResponses.get(feedSymbol)!;
return {
s2: prices[0],
s3: prices[1],
ema: decNToFloat(s2EmaInfo?.price ?? 0n, -(s2EmaInfo?.expo ?? 1)),
s2MktClosed: mktClosed[0],
s3MktClosed: mktClosed[1],
conf: BigInt(s2Info?.conf ?? 0n),
predMktCLOBParams: s2EmaInfo?.conf ?? 0n,
} as IdxPriceInfo;
} else {
return {
s2: prices[0],
s3: prices[1],
ema: prices[0],
s2MktClosed: mktClosed[0],
s3MktClosed: mktClosed[1],
conf: BigInt(0),
predMktCLOBParams: BigInt(0),
} as IdxPriceInfo;
}
}
/**
* Fetch the provided feed prices and bool whether market is closed or open
* - requires the feeds to be defined in priceFeedConfig.json
* - vaas are not of interest here, therefore only readonly price feed
* endpoints are used
* @param symbols array of feed-price symbols (e.g., [btc-usd, eth-usd])
* @returns mapping symbol-> [price, isMarketClosed], also has an entry
* <symbol>:ema for each polymarket symbol that maps to the ema price
*/
public async fetchFeedPrices(symbols: string[]): Promise<Map<string, [number, boolean]>> {
const result = new Map();
try {
const resps = await this.fetchFeedPriceResponses(symbols);
for (const [key, value] of resps) {
result.set(key, [value[0], value[1]]);
}
} catch (e) {
console.log("fetchFeedPriceResponses failed with args", symbols, e);
}
return result;
}
public async fetchFeedPriceResponses(
symbols: string[]
): Promise<Map<string, [number, boolean, PriceFeedFormat | undefined, PriceFeedFormat | undefined]>> {
if (this.config == undefined) {
throw Error("init() required");
}
let queries = new Array<string>(this.feedEndpoints.length);
let suffixes = new Array<string>(queries.length);
let symbolsOfEndpoint: string[][] = [];
for (let j = 0; j < queries.length; j++) {
symbolsOfEndpoint.push([]);
}
let onChainSyms: string[] = [];
let polyMktSyms: string[] = [];
for (let k = 0; k < this.config.ids.length; k++) {
let currFeed = this.config.ids[k];
if (!symbols.includes(currFeed.symbol)) {
continue;
}
if (currFeed.type == "onchain") {
onChainSyms.push(currFeed.symbol);
continue;
}
if (currFeed.type == "polymarket") {
polyMktSyms.push(currFeed.symbol);
continue;
}
const apiFormat = { pyth: this.PYTH, odin: this.PYTH, v2: this.PYTH, v3: this.PYTH, "low-liq": this.PYTH }[
currFeed.type
];
if (apiFormat === undefined) {
throw new Error(`API format for ${currFeed} unknown.`);
}
// feedInfo: Map<string, {symbol:string, endpointId: number}[]>; // priceFeedId -> (symbol, endpointId)[]
let endpointId = this.feedInfo.get(currFeed.id)![0].endpointId;
symbolsOfEndpoint[endpointId].push(currFeed.symbol);
if (queries[endpointId] == undefined) {
// each id can have a different endpoint, but we cluster
// the queries into one per endpoint
queries[endpointId] = this.feedEndpoints[endpointId] + apiFormat.endpoint + currFeed.id;
suffixes[endpointId] = apiFormat.suffix;
} else {
queries[endpointId] = queries[endpointId] + apiFormat.separator + currFeed.id;
}
}
let onChainPromise = this.queryOnChainPxFeeds(onChainSyms);
let polyMktsPromise = this.queryPolyMktsPxFeeds(polyMktSyms);
let resultPrices = new Map<string, [number, boolean, PriceFeedFormat | undefined, PriceFeedFormat | undefined]>();
for (let k = 0; k < queries.length; k++) {
if (queries[k] == undefined) {
continue;
}
const { price: pxInfo, emaPrice } = await this.fetchFullPriceQuery(queries[k] + suffixes[k]);
let tsSecNow = Math.round(Date.now() / 1000);
for (let j = 0; j < pxInfo.length; j++) {
let price = decNToFloat(BigInt(pxInfo[j].price), -pxInfo[j].expo);
let isMarketClosed = tsSecNow - pxInfo[j].publish_time > this.THRESHOLD_MARKET_CLOSED_SEC;
resultPrices.set(symbolsOfEndpoint[k][j], [price, isMarketClosed, pxInfo[j], emaPrice[j]]);
}
}
let onChPxs = await onChainPromise;
for (let k = 0; k < onChainSyms.length; k++) {
let sym = onChainSyms[k];
resultPrices.set(sym, [onChPxs[k], false, undefined, undefined]);
}
let polyPxs = await polyMktsPromise;
for (let k = 0; k < polyPxs.length; k++) {
let sym = polyMktSyms[k];
if (polyPxs[k] == undefined) {
continue;
}
resultPrices.set(sym, [polyPxs[k].s2, polyPxs[k].s2MktClosed, undefined, undefined]);
resultPrices.set(sym + ":ema", [polyPxs[k].ema, polyPxs[k].s2MktClosed, undefined, undefined]);
}
return resultPrices;
}
private async queryOnChainPxFeeds(symbols: string[]) {
let prices: number[] = new Array<number>();
for (let k = 0; k < symbols.length; k++) {
let sym = symbols[k];
const feed = this.onChainPxFeeds.get(sym);
let price = (await feed?.getPrice()) || 0;
prices.push(price);
}
return prices;
}
// returns an array with two values per symbol: price, ema
private async queryPolyMktsPxFeeds(symbols: string[]): Promise<PredMktPriceInfo[]> {
if (symbols.length == 0) {
return [];
}
if (this.polyMktsPxFeed == undefined) {
throw Error("init() required");
}
let prices;
let trial = 0;
while (true) {
try {
prices = await this.polyMktsPxFeed.fetchPricesForSyms(symbols);
} catch (error) {
if (trial > 4) {
throw error;
}
console.log("fetchPriceForSym failed for " + symbols);
console.log(error);
trial++;
await sleepForSec(1); //seconds
continue;
}
break;
}
return prices;
}
/**
* Get all required feed prices via "latest_price_feeds".
* 'required' means part of perpetuals/triangulations
* @returns map of feed-price symbol to price/isMarketClosed
*/
public async fetchAllFeedPrices(): Promise<Map<string, [number, boolean]>> {
return this.fetchFeedPrices(this.dataHandler.requiredSymbols);
}
private _buildQuery(baseUrl: string, id: string) {
// odin-sports use v3 api, all else v2
let method = "";
if (baseUrl.includes("odin-sport")) {
method = "/v3/updates/price/latest?encoding=base64&ids[]=";
} else {
method = "/v2/updates/price/latest?encoding=base64&ids[]=";
}
// e.g.
// 'https://odin-sport.quantena.org/v3/updates/price/latest?encoding=base64&ids[]=0x5b622123024d99bea493662fe91bf9785b8b0decd46c65cdbeef6a0a1672b057',
// 'https://hermes.pyth.network/v2/updates/price/latest?encoding=base64&ids[]=0x2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b'
return baseUrl + method + id;
}
/**
* Get the latest prices for a given perpetual from the offchain oracle
* networks. Uses write price feed endpoints.
* @param symbol perpetual symbol of the form BTC-USD-MATIC
* @returns array of price feed updates that can be submitted to the smart
* contract and corresponding price information
*/
public async fetchLatestFeedPriceInfoForPerpetual(symbol: string): Promise<PriceFeedSubmission> {
if (this.config == undefined) {
throw Error("init() required");
}
// get the feedIds that the contract uses
let feedIds = this.dataHandler.getPriceIds(symbol);
let queries = new Array<string>();
let symbols = new Map<string, string[]>();
for (let k = 0; k < feedIds.length; k++) {
let info = this.feedInfo.get(feedIds[k]);
if (info == undefined) {
throw new Error(`priceFeeds: config for symbol ${symbol} insufficient`);
}
// we use the first endpoint for a given symbol even if there is another symbol with the same id
let idx = info[0].endpointId;
let feedId = feedIds[k];
queries.push(this._buildQuery(this.writeFeedEndpoints[idx], feedId));
for (let j = 0; j < info.length; j++) {
if (symbols.has(feedId)) {
let v = symbols.get(feedId)!;
v.push(info[j].symbol);
symbols.set(feedId, v);
} else {
symbols.set(feedId, [info[j].symbol]);
}
}
}
let data;
try {
data = await Promise.all(
queries.map(async (q) => {
if (q != undefined) {
return this.fetchVAAQuery(q);
} else {
return { vaas: [], prices: [] };
}
})
);
} catch (error) {
// try switching endpoints and re-query
console.log("fetchVAAQuery failed, selecting random price feed endpoint...");
[this.feedInfo, this.feedEndpoints] = PriceFeeds._constructFeedInfo(this.config, true);
data = await Promise.all(
queries.map(async (q) => {
if (q != undefined) {
return this.fetchVAAQuery(q);
} else {
return { vaas: [], prices: [] };
}
})
);
console.log("success");
}
const priceFeedUpdates = new Array<string>();
const prices = new Array<number>();
const mktClosed = new Array<boolean>();
const timestamps = new Array<number>();
const tsSecNow = Math.round(Date.now() / 1000);
for (let k = 0; k < feedIds.length; k++) {
let pxInfo: PriceFeedFormat = data[k].prices[0];
let price = decNToFloat(BigInt(pxInfo.price), -pxInfo.expo);
prices.push(price);
priceFeedUpdates.push(data[k].vaas[0]);
let isMarketClosed = pxInfo.price == 0n || tsSecNow - pxInfo.publish_time > this.THRESHOLD_MARKET_CLOSED_SEC;
mktClosed.push(isMarketClosed);
timestamps.push(pxInfo.publish_time);
}
return {
symbols: symbols,
ids: feedIds,
priceFeedVaas: priceFeedUpdates,
prices: prices,
isMarketClosed: mktClosed,
timestamps: timestamps,
};
}
/**
* Extract pair-prices from underlying price feeds via triangulation
* The function either needs a direct price feed or a defined triangulation to succesfully
* return a triangulated price
* @param symbols array of pairs for which we want prices, e.g., [BTC-USDC, ETH-USD]
* @param feeds data obtained via fetchLatestFeedPriceInfo or fetchLatestFeedPrices
* @returns array of prices with same order as symbols
*/
public calculateTriangulatedPricesFromFeedInfo(symbols: string[], feeds: PriceFeedSubmission): [number[], boolean[]] {
let priceMap = new Map<string, [number, boolean]>();
for (let j = 0; j < feeds.prices.length; j++) {
const syms = feeds.symbols.get(feeds.ids[j]);
if (syms == undefined) {
console.log("calculateTriangulatedPricesFromFeedInfo: could not find symbol for id ", feeds.ids[j]);
continue;
}
for (let k = 0; k < syms.length; k++) {
// add price feed for all symbols that use this id
priceMap.set(syms[k], [feeds.prices[j], feeds.isMarketClosed[j]]);
}
}
return this.triangulatePricesFromFeedPrices(symbols, priceMap);
}
/**
* Extract pair-prices from underlying price feeds via triangulation
* The function either needs a direct price feed or a defined triangulation to succesfully
* return a triangulated price
* @param symbols array of pairs for which we want prices, e.g., [BTC-USDC, ETH-USD]
* @param feeds data obtained via fetchLatestFeedPriceInfo or fetchLatestFeedPrices
* @returns array of prices with same order as symbols
*/
public triangulatePricesFromFeedPrices(
symbols: string[],
feedPriceMap: Map<string, [number, boolean]>
): [number[], boolean[]] {
let prices = new Array<number>();
let mktClosed = new Array<boolean>();
for (let k = 0; k < symbols.length; k++) {
let sym = symbols[k] as string;
let triangulation: [string[], boolean[]] | undefined = this.triangulations.get(sym);
if (triangulation == undefined) {
let feedPrice = feedPriceMap.get(sym);
if (feedPrice == undefined) {
throw new Error(`PriceFeeds: no triangulation defined for ${sym}`);
} else {
prices.push(feedPrice[0]); //price
mktClosed.push(feedPrice[1]); //market closed?
continue;
}
}
let [px, isMktClosed]: [number, boolean] = Triangulator.calculateTriangulatedPrice(triangulation, feedPriceMap);
prices.push(px);
mktClosed.push(isMktClosed);
}
return [prices, mktClosed];
}
/**
* Queries the REST endpoint and returns parsed VAA price data
* We expect one single id in the query,
* otherwise the VAA is a compressed VAA for all prices which is not suited
* for the smart contracts
* @param query query price-info from endpoint
* @returns vaa and price info
*/
private async fetchVAAQuery(query: string): Promise<{ vaas: string[]; prices: PriceFeedFormat[] }> {
const headers = { headers: { "Content-Type": "application/json" } };
let response = await fetch(query, headers);
if (!response.ok) {
throw new Error(`Failed to fetch posts (${response.status}): ${response.statusText} ${query}`);
}
let values = (await response.json()) as PythV2LatestPriceFeed;
const vaas = new Array<string>();
const prices = new Array<PriceFeedFormat>();
for (let k = 0; k < values.parsed.length; k++) {
// see also fetchPriceQuery for idx
const idx = k % values.binary.data.length;
vaas.push("0x" + Buffer.from(values.binary.data[idx], "base64").toString("hex"));
// check price data
const price = values.parsed[k].price;
if (price.price.toString() == "NaN") {
price.price = 0n;
}
prices.push(price);
}
return { vaas, prices };
}
/**
* Queries the REST endpoint and returns parsed price data
* @param query query price-info from endpoint
* @returns array of vaa and price info
*/
public async fetchPriceQuery(query: string): Promise<[string[], PriceFeedFormat[]]> {
const fullResp = await this.fetchFullPriceQuery(query);
return [fullResp.updateData, fullResp.price];
}
/**
* Queries the REST endpoint and returns parsed price data
* @param query query price-info from endpoint
* @returns object with vaa and price info (both spot and ema)
*/
public async fetchFullPriceQuery(
query: string
): Promise<{ updateData: string[]; price: PriceFeedFormat[]; emaPrice: PriceFeedFormat[] }> {
let values: PythV2LatestPriceFeed;
const cached = this.cache.get(query);
const tsNow = Date.now() / 1_000;
if (cached && cached.timestamp + 1 > tsNow) {
// less than one second has passed since the last query - no need to query again
values = cached.values;
} else {
const headers = { headers: { "Content-Type": "application/json" } };
let response = await fetch(query, headers);
if (!response.ok) {
throw new Error(`Failed to fetch posts (${response.status}): ${response.statusText} query:${query}`);
}
values = await response.json();
this.cache.set(query, { timestamp: tsNow, values: values });
}
const priceFeedUpdates = new Array<string>();
const px = new Array<PriceFeedFormat>();
const emaPx = new Array<PriceFeedFormat>();
for (let k = 0; k < values.parsed.length; k++) {
// pyth v2 only provides one compressed vaa when querying multiple prices.
// contracts can only use separate ones. In case we get only one vaa,
// here we add the single vaa in each price-update
const idx = k % values.binary.data.length;
priceFeedUpdates.push("0x" + Buffer.from(values.binary.data[idx], "base64").toString("hex"));
px.push(values.parsed[k].price as PriceFeedFormat);
emaPx.push(values.parsed[k].ema_price as PriceFeedFormat);
}
return { updateData: priceFeedUpdates, price: px, emaPrice: emaPx };
}
/**
* Searches for configuration for given network
* @param configs pricefeed configuration from json
* @param network e.g. testnet
* @returns selected configuration
*/
static _selectConfig(configs: PriceFeedConfig[], network: string): PriceFeedConfig {
let k = 0;
while (k < configs.length) {
if (configs[k].network == network) {
return configs[k];
}
k = k + 1;
}
throw new Error(`PriceFeeds: config not found for network ${network}`);
}
/**
* Wraps configuration into convenient data-structure
* @param config configuration for the selected network
* @returns feedInfo-map and endPoints-array
*/
static _constructFeedInfo(
config: PriceFeedConfig,
shuffleEndpoints: boolean
): [Map<string, { symbol: string; endpointId: number }[]>, string[], string[]] {
let feed = new Map<string, [{ symbol: string; endpointId: number }]>();
let endpointId = -1;
let type = "";
let feedEndpoints = new Array<string>();
let writeFeedEndpoints = new Array<string>();
for (let k = 0; k < config.endpoints.length; k++) {
const L = config.endpoints[k].endpoints.length;
let endpointNr = !shuffleEndpoints ? 0 : 1 + Math.floor(Math.random() * (L - 1));
// if config has only one endpoint:
endpointNr = Math.min(endpointNr, L - 1);
feedEndpoints.push(config.endpoints[k].endpoints[endpointNr]);
// write endpoints
const n = config.endpoints[k].writeEndpoints.length;
const useEndpoint = Math.min(!shuffleEndpoints ? 0 : 1 + Math.floor(Math.random() * (n - 1)), n - 1);
writeFeedEndpoints.push(config.endpoints[k].writeEndpoints[useEndpoint]);
}
for (let k = 0; k < config.ids.length; k++) {
if (type != config.ids[k].type) {
type = config.ids[k].type;
let j = 0;
while (j < config.endpoints.length) {
if (config.endpoints[j].type == type) {
endpointId = j;
j = config.endpoints.length;
}
j++;
}
if (config.endpoints[endpointId].type != type) {
throw new Error(`priceFeeds: no endpoint found for ${type} check priceFeedConfig`);
}
}
// one id can have multiple symbols pointing to it
const id = config.ids[k].id;
const item = { symbol: config.ids[k].symbol.toUpperCase(), endpointId: endpointId };
if (feed.has(id)) {
feed.get(id)?.push(item);
} else {
feed.set(id, [item]);
}
}
return [feed, feedEndpoints, writeFeedEndpoints];
}
}