UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

224 lines (217 loc) 6.36 kB
import type { PriceUpdate, PythMessageParts } from './types.js'; import type { ParsedFeedPayload } from '@pythnetwork/pyth-lazer-sdk'; import { MARKET_SESSION_ORDER, PRICE_UPDATE_MAGIC, PROP_ID, SOLANA_FORMAT_MAGIC, } from './types.js'; function expectMagic( buf: Uint8Array, offset: number, magic: Uint8Array, label: string, ): void { for (let i = 0; i < magic.length; i++) { if (buf[offset + i] !== magic[i]) { throw new Error(`Invalid ${label} magic byte at ${i}`); } } } function bigintToSdkNumber(v: bigint): number { if ( v > BigInt(Number.MAX_SAFE_INTEGER) || v < BigInt(Number.MIN_SAFE_INTEGER) ) { throw new RangeError('Scalar does not fit in a safe integer'); } return Number(v); } function decodeFeedPayload( buf: Uint8Array, view: DataView, start: number, ): { feed: ParsedFeedPayload; next: number } { let o = start; if (o + 5 > buf.length) throw new RangeError('Truncated feed header'); const priceFeedId = view.getUint32(o, true); o += 4; const nProps = buf[o]; o += 1; const feed: ParsedFeedPayload = { priceFeedId }; for (let i = 0; i < nProps; i++) { if (o >= buf.length) throw new RangeError('Truncated feed property header'); const propId = buf[o]; o += 1; switch (propId) { case PROP_ID.Price: { const v = view.getBigInt64(o, true); o += 8; if (v !== 0n) feed.price = v.toString(); break; } case PROP_ID.BestBidPrice: { const v = view.getBigInt64(o, true); o += 8; if (v !== 0n) feed.bestBidPrice = v.toString(); break; } case PROP_ID.BestAskPrice: { const v = view.getBigInt64(o, true); o += 8; if (v !== 0n) feed.bestAskPrice = v.toString(); break; } case PROP_ID.PublisherCount: { feed.publisherCount = view.getUint16(o, true); o += 2; break; } case PROP_ID.Exponent: { feed.exponent = view.getInt16(o, true); o += 2; break; } case PROP_ID.Confidence: { const v = view.getBigInt64(o, true); o += 8; if (v !== 0n) feed.confidence = bigintToSdkNumber(v); break; } case PROP_ID.FundingRate: { const tag = buf[o]; o += 1; if (tag === 1) { const v = view.getBigInt64(o, true); o += 8; feed.fundingRate = bigintToSdkNumber(v); } else if (tag !== 0) { throw new RangeError('Invalid FundingRate option tag'); } break; } case PROP_ID.FundingTimestamp: { const tag = buf[o]; o += 1; if (tag === 1) { const v = view.getBigUint64(o, true); o += 8; feed.fundingTimestamp = bigintToSdkNumber(v); } else if (tag !== 0) { throw new RangeError('Invalid FundingTimestamp option tag'); } break; } case PROP_ID.FundingRateInterval: { const tag = buf[o]; o += 1; if (tag === 1) { const v = view.getBigUint64(o, true); o += 8; feed.fundingRateInterval = bigintToSdkNumber(v); } else if (tag !== 0) { throw new RangeError('Invalid FundingRateInterval option tag'); } break; } case PROP_ID.MarketSession: { const idx = view.getUint16(o, true); o += 2; const session = MARKET_SESSION_ORDER[idx]; if (!session) throw new RangeError('Invalid marketSession index'); feed.marketSession = session; break; } case PROP_ID.EmaPrice: { const v = view.getBigInt64(o, true); o += 8; if (v !== 0n) feed.emaPrice = v.toString(); break; } case PROP_ID.EmaConfidence: { const v = view.getBigInt64(o, true); o += 8; if (v !== 0n) feed.emaConfidence = bigintToSdkNumber(v); break; } case PROP_ID.FeedUpdateTimestamp: { const tag = buf[o]; o += 1; if (tag === 1) { const v = view.getBigUint64(o, true); o += 8; feed.feedUpdateTimestamp = bigintToSdkNumber(v); } else if (tag !== 0) { throw new RangeError('Invalid FeedUpdateTimestamp option tag'); } break; } default: throw new RangeError(`Unknown feed property id: ${propId}`); } } return { feed, next: o }; } /** * Decode a PriceUpdate payload produced by {@link encodePriceUpdate}. */ export function decodePriceUpdate(payload: Uint8Array): PriceUpdate { const min = 4 + 8 + 1 + 1; if (payload.length < min) { throw new RangeError('Price update payload too short'); } expectMagic(payload, 0, PRICE_UPDATE_MAGIC, 'price update'); const view = new DataView( payload.buffer, payload.byteOffset, payload.byteLength, ); let o = 4; const timestampUs = view.getBigUint64(o, true); o += 8; const channelId = payload[o]; o += 1; const feedsLen = payload[o]; o += 1; const priceFeeds: ParsedFeedPayload[] = []; for (let f = 0; f < feedsLen; f++) { const decoded = decodeFeedPayload(payload, view, o); priceFeeds.push(decoded.feed); o = decoded.next; } if (o !== payload.length) { throw new RangeError('Trailing bytes in price update payload'); } return { timestampUs: timestampUs.toString(), channelId, priceFeeds, }; } const PYTH_MESSAGE_HEADER_LEN = 4 + 64 + 32 + 2; /** * Decode a Solana-format Pyth message produced by {@link encodePythMessage}. */ export function decodePythMessage(message: Uint8Array): PythMessageParts { if (message.length < PYTH_MESSAGE_HEADER_LEN) { throw new RangeError('Pyth message too short'); } expectMagic(message, 0, SOLANA_FORMAT_MAGIC, 'Solana format'); const view = new DataView( message.buffer, message.byteOffset, message.byteLength, ); const payloadLen = view.getUint16(100, true); const total = PYTH_MESSAGE_HEADER_LEN + payloadLen; if (message.length !== total) { throw new RangeError( `Pyth message length mismatch: expected ${total} bytes, got ${message.length}`, ); } return { signature: message.subarray(4, 68).slice(), publicKey: message.subarray(68, 100).slice(), payload: message.subarray(102, total).slice(), }; }