UNPKG

@d8x/perpetuals-sdk

Version:

Node TypeScript SDK for D8X Perpetual Futures

714 lines 31.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const buffer_1 = require("buffer"); const d8XMath_1 = require("./d8XMath"); const onChainPxFactory_1 = __importDefault(require("./onChainPxFactory")); const polyMktsPxFeed_1 = __importDefault(require("./polyMktsPxFeed")); const triangulator_1 = __importDefault(require("./triangulator")); const utils_1 = require("./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. */ class PriceFeeds { constructor(dataHandler, priceFeedConfigNetwork) { // Read only price info endpoints. Used by default. feedEndpoints[endpointId] // = endpointstring this.feedEndpoints = []; // Endpoints which are used to fetch prices for submissions this.writeFeedEndpoints = []; this.THRESHOLD_MARKET_CLOSED_SEC = 15; // smallest lag for which we consider the market as being closed this.cache = new Map(); // api formatting constants this.PYTH = { endpoint: "/v2/updates/price/latest?encoding=base64&ids[]=", separator: "&ids[]=", suffix: "" }; this.priceFeedConfigNetwork = priceFeedConfigNetwork; this.onChainPxFeeds = new Map(); this.dataHandler = dataHandler; this.triangulations = new Map(); this.feedInfo = new Map(); } /** * initialization function. Gathers config from config-hub if url * specified */ async init() { let configs; const configSrc = this.dataHandler.config.configSource; if (configSrc == "" || configSrc == undefined) { // embedded config configs = require("./config/priceFeedConfig.json"); } 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_1.default.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_1.default(this.config); } getConfig() { 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. static overridePriceEndpointsOfSameType(configEndpoints, userProvidedEndpoints) { 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 */ static trimEndpoint(endp) { // 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 */ initializeTriangulations(symbols) { let feedSymbols = new Array(); 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_1.default.triangulate(feedSymbols, symbol); this.triangulations.set(symbol, triangulation); } } /** * Returns computed triangulation map * @returns Triangulation map */ getTriangulations() { return this.triangulations; } /** * Set pre-computed triangulation map */ setTriangulations(triangulation) { 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 */ async fetchFeedPriceInfoAndIndicesForPerpetual(symbol) { let indexSymbols = await this.dataHandler.getIndexSymbols(symbol); // fetch prices from required price-feeds (REST) let submission = await this.fetchLatestFeedPriceInfoForPerpetual(symbol); // calculate index-prices from price-feeds let [_idxPrices, _mktClosed] = this.calculateTriangulatedPricesFromFeedInfo(indexSymbols.filter((x) => x != ""), submission); let idxPrices = [_idxPrices[0], 0]; let mktClosed = [_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 */ async fetchPrices(symbols) { let feedPrices = await this.fetchAllFeedPrices(); let [prices, mktClosed] = this.triangulatePricesFromFeedPrices(symbols, feedPrices); let symMap = new Map(); 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. */ async fetchPricesForPerpetual(symbol) { 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 = { 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(); for (let sym of indexSymbols) { if (sym != "") { let triang = 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: (0, d8XMath_1.decNToFloat)(s2EmaInfo?.price ?? 0n, -(s2EmaInfo?.expo ?? 1)), s2MktClosed: mktClosed[0], s3MktClosed: mktClosed[1], conf: BigInt(s2Info?.conf ?? 0n), predMktCLOBParams: s2EmaInfo?.conf ?? 0n, }; } else { return { s2: prices[0], s3: prices[1], ema: prices[0], s2MktClosed: mktClosed[0], s3MktClosed: mktClosed[1], conf: BigInt(0), predMktCLOBParams: BigInt(0), }; } } /** * 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 */ async fetchFeedPrices(symbols) { 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; } async fetchFeedPriceResponses(symbols) { if (this.config == undefined) { throw Error("init() required"); } let queries = new Array(this.feedEndpoints.length); let suffixes = new Array(queries.length); let symbolsOfEndpoint = []; for (let j = 0; j < queries.length; j++) { symbolsOfEndpoint.push([]); } let onChainSyms = []; let polyMktSyms = []; 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(); 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 = (0, d8XMath_1.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; } async queryOnChainPxFeeds(symbols) { let prices = new Array(); 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 async queryPolyMktsPxFeeds(symbols) { 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 (0, utils_1.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 */ async fetchAllFeedPrices() { return this.fetchFeedPrices(this.dataHandler.requiredSymbols); } _buildQuery(baseUrl, id) { // 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 */ async fetchLatestFeedPriceInfoForPerpetual(symbol) { 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(); let symbols = new Map(); 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(); const prices = new Array(); const mktClosed = new Array(); const timestamps = new Array(); const tsSecNow = Math.round(Date.now() / 1000); for (let k = 0; k < feedIds.length; k++) { let pxInfo = data[k].prices[0]; let price = (0, d8XMath_1.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 */ calculateTriangulatedPricesFromFeedInfo(symbols, feeds) { let priceMap = new Map(); 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 */ triangulatePricesFromFeedPrices(symbols, feedPriceMap) { let prices = new Array(); let mktClosed = new Array(); for (let k = 0; k < symbols.length; k++) { let sym = symbols[k]; let triangulation = 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] = triangulator_1.default.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 */ async fetchVAAQuery(query) { 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()); const vaas = new Array(); const prices = new Array(); 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_1.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 */ async fetchPriceQuery(query) { 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) */ async fetchFullPriceQuery(query) { let values; const cached = this.cache.get(query); const tsNow = Date.now() / 1000; 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(); const px = new Array(); const emaPx = new Array(); 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_1.Buffer.from(values.binary.data[idx], "base64").toString("hex")); px.push(values.parsed[k].price); emaPx.push(values.parsed[k].ema_price); } 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, network) { 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, shuffleEndpoints) { let feed = new Map(); let endpointId = -1; let type = ""; let feedEndpoints = new Array(); let writeFeedEndpoints = new Array(); 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]; } } exports.default = PriceFeeds; //# sourceMappingURL=priceFeeds.js.map