import { HexString, HermesClient, PriceUpdate, } from "@pythnetwork/hermes-client"; import { PriceInfo, IPriceListener, PriceItem } from "./interface"; import { Logger } from "pino"; type TimestampInMs = number & { readonly _: unique symbol }; export class PythPriceListener implements IPriceListener { private hermesClient: HermesClient; private priceIds: HexString[]; private priceIdToAlias: Map; private latestPriceInfo: Map; private logger: Logger; private lastUpdated: TimestampInMs | undefined; private healthCheckInterval?: NodeJS.Timeout; constructor( hermesClient: HermesClient, priceItems: PriceItem[], logger: Logger ) { this.hermesClient = hermesClient; this.priceIds = priceItems.map((priceItem) => priceItem.id); this.priceIdToAlias = new Map( priceItems.map((priceItem) => [priceItem.id, priceItem.alias]) ); this.latestPriceInfo = new Map(); this.logger = logger; } // This method should be awaited on and once it finishes it has the latest value // for the given price feeds (if they exist). async start() { const eventSource = await this.hermesClient.getPriceUpdatesStream( this.priceIds, { parsed: true, ignoreInvalidPriceIds: true, } ); eventSource.onmessage = (event: MessageEvent) => { const priceUpdates = JSON.parse(event.data) as PriceUpdate; priceUpdates.parsed?.forEach((priceUpdate) => { this.logger.debug( `Received new price feed update from Pyth price service: ${this.priceIdToAlias.get( priceUpdate.id )} ${priceUpdate.id}` ); // Consider price to be currently available if it is not older than 60s const currentPrice = Date.now() / 1000 - priceUpdate.price.publish_time > 60 ? undefined : priceUpdate.price; if (currentPrice === undefined) { this.logger.debug("Price is older than 60s, skipping"); return; } const priceInfo: PriceInfo = { conf: currentPrice.conf, price: currentPrice.price, publishTime: currentPrice.publish_time, }; this.latestPriceInfo.set(priceUpdate.id, priceInfo); this.lastUpdated = Date.now() as TimestampInMs; }); }; eventSource.onerror = (error: Event) => { console.error("Error receiving updates from Hermes:", error); eventSource.close(); }; // Store health check interval reference this.healthCheckInterval = setInterval(() => { if ( this.lastUpdated === undefined || this.lastUpdated < Date.now() - 30 * 1000 ) { throw new Error("Hermes Price feeds are not updating."); } }, 5000); } getLatestPriceInfo(priceId: HexString): PriceInfo | undefined { return this.latestPriceInfo.get(priceId); } cleanup() { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); } } }