import { HERMES_URL, SOL_USD_FEED_ID } from "../constants.js"; import { PythPrice, HermesPriceUpdate } from "./types.js"; import { calculateOutcome as _calculateOutcome, shouldMarkExtreme as _shouldMarkExtreme, } from "../outcome/calculate.js"; import type { OutcomeResult } from "../outcome/types.js"; import BN from "bn.js"; /** * Pyth Price Service * * Fetches SOL/USD prices from Pyth Hermes API for: * - Displaying current prices * - Checking if price crosses strikes * - Getting price update data for on-chain verification * * @example * ```typescript * const priceService = new PythPriceService(); * * // Get current price * const price = await priceService.getLatestPrice(); * console.log(`SOL/USD: $${price.price}`); * * // Get price update data for on-chain posting * const updateData = await priceService.getPriceUpdateData(); * ``` */ export class PythPriceService { private hermesUrl: string; private feedId: string; /** * Create a new PythPriceService * * @param hermesUrl - Hermes API URL (default: https://hermes.pyth.network) * @param feedId - Pyth feed ID (default: SOL/USD) */ constructor(hermesUrl: string = HERMES_URL, feedId: string = SOL_USD_FEED_ID) { this.hermesUrl = hermesUrl; this.feedId = feedId; } /** * Get the latest SOL/USD price * * @returns Parsed price data * @throws Error if fetch fails or price is not available */ async getLatestPrice(): Promise { const url = `${this.hermesUrl}/v2/updates/price/latest?ids[]=${this.feedId}`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch price: ${response.statusText}`); } const data = (await response.json()) as HermesPriceUpdate; if (!data.parsed || data.parsed.length === 0) { throw new Error("No price data returned from Hermes"); } const parsed = data.parsed[0]; const price = parseInt(parsed.price.price, 10); const expo = parsed.price.expo; const conf = parseInt(parsed.price.conf, 10); // Convert to USD: price * 10^expo const priceUsd = price * Math.pow(10, expo); const confUsd = conf * Math.pow(10, expo); // Convert to cents: multiply by 100 const priceInCents = Math.round(priceUsd * 100); return { price: priceUsd, priceInCents, confidence: confUsd, timestamp: parsed.price.publish_time, expo, }; } /** * Get price update data for posting to Pyth Solana Receiver * * This returns the VAA (Verified Action Approval) data that needs to be * posted on-chain before calling mark_extreme. * * @returns Base64-encoded price update data * @throws Error if fetch fails */ async getPriceUpdateData(): Promise { const url = `${this.hermesUrl}/v2/updates/price/latest?ids[]=${this.feedId}&encoding=base64`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch price update: ${response.statusText}`); } const data = (await response.json()) as HermesPriceUpdate; if (!data.binary || !data.binary.data || data.binary.data.length === 0) { throw new Error("No binary data returned from Hermes"); } return data.binary.data; } /** * Calculate the expected outcome based on current price and strikes * * @deprecated Use calculateOutcome from @pit-protocol/sdk/outcome instead * * @param priceInCents - Current price in cents * @param strikes - Strike prices in cents [strike0, strike1, strike2] * @returns Outcome for both High and Low pools */ calculateOutcome( priceInCents: number, strikes: [BN, BN, BN] | [number, number, number] ): OutcomeResult { if (process.env.NODE_ENV !== "test") { console.warn( "[DEPRECATION] PythPriceService.calculateOutcome is deprecated. " + "Use calculateOutcome from '@pit-protocol/sdk/outcome' instead." ); } return _calculateOutcome(priceInCents, strikes); } /** * Check if a new outcome should be marked * * @deprecated Use shouldMarkExtreme from @pit-protocol/sdk/outcome instead * * @param currentOutcome - Current winning_outcome from pool * @param newOutcome - Calculated outcome from price * @returns true if newOutcome > currentOutcome (should call mark_extreme) */ shouldMarkExtreme(currentOutcome: number, newOutcome: number): boolean { if (process.env.NODE_ENV !== "test") { console.warn( "[DEPRECATION] PythPriceService.shouldMarkExtreme is deprecated. " + "Use shouldMarkExtreme from '@pit-protocol/sdk/outcome' instead." ); } return _shouldMarkExtreme(currentOutcome, newOutcome); } }