UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

263 lines (244 loc) 8.48 kB
import type { Feed, MarketSession, 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'; export { PRICE_UPDATE_MAGIC, SOLANA_FORMAT_MAGIC } from './types.js'; function u8(b: number): Uint8Array { if (b < 0 || b > 255) throw new RangeError('u8 out of range'); return new Uint8Array([b & 0xff]); } function u16Le(n: number): Uint8Array { if (n < 0 || n > 0xffff) throw new RangeError('u16 out of range'); const buf = new Uint8Array(2); new DataView(buf.buffer).setUint16(0, n, true); return buf; } function u32Le(n: number): Uint8Array { if (n < 0 || n > 0xffff_ffff) throw new RangeError('u32 out of range'); const buf = new Uint8Array(4); new DataView(buf.buffer).setUint32(0, n, true); return buf; } function u64Le(n: bigint): Uint8Array { const buf = new Uint8Array(8); const view = new DataView(buf.buffer); const lo = Number(n & 0xffff_ffff_ffff_ffffn); const hi = Number((n >> 32n) & 0xffff_ffffn); view.setUint32(0, lo & 0xffff_ffff, true); view.setUint32(4, hi, true); return buf; } function i16Le(n: number): Uint8Array { const buf = new Uint8Array(2); new DataView(buf.buffer).setInt16(0, n, true); return buf; } function i64Le(n: bigint): Uint8Array { const buf = new Uint8Array(8); const view = new DataView(buf.buffer); const lo = Number(n & 0xffff_ffff_ffff_ffffn); const hi = Number(Number(n >> 32n) & 0xffff_ffff); view.setUint32(0, lo & 0xffff_ffff, true); view.setUint32(4, hi, true); return buf; } function concat(...chunks: Uint8Array[]): Uint8Array { const total = chunks.reduce((s, c) => s + c.length, 0); const out = new Uint8Array(total); let off = 0; for (const c of chunks) { out.set(c, off); off += c.length; } return out; } /** Encode a single feed property (property_id byte + value bytes). */ function encodeFeedProperty(id: number, value: Uint8Array): Uint8Array { return concat(u8(id), value); } function toFeed(parsed: ParsedFeedPayload): Feed { const toBigInt = (v: string | number | undefined): bigint | null => v === undefined ? null : BigInt(v); const toBigIntOptional = (v: string | number | undefined): bigint | null => v === undefined ? null : BigInt(v); return { feedId: parsed.priceFeedId, price: parsed.price === undefined ? null : BigInt(parsed.price), bestBidPrice: toBigIntOptional(parsed.bestBidPrice), bestAskPrice: toBigIntOptional(parsed.bestAskPrice), publisherCount: parsed.publisherCount ?? 0, exponent: parsed.exponent ?? 0, confidence: toBigInt(parsed.confidence), fundingRate: toBigInt(parsed.fundingRate), fundingTimestamp: toBigInt(parsed.fundingTimestamp), fundingRateInterval: toBigInt(parsed.fundingRateInterval), marketSession: (parsed.marketSession as MarketSession) ?? 'Regular', emaPrice: toBigIntOptional(parsed.emaPrice), emaConfidence: toBigInt(parsed.emaConfidence), feedUpdateTimestamp: toBigInt(parsed.feedUpdateTimestamp), }; } /** Encode feed properties that are defined, in the order expected by the parser. */ function encodeFeed(feed: Feed): Uint8Array { const parts: Uint8Array[] = []; if (feed.price !== undefined) { const v = feed.price === null ? 0n : feed.price; parts.push(encodeFeedProperty(PROP_ID.Price, i64Le(v))); } if (feed.bestBidPrice !== undefined) { const v = feed.bestBidPrice === null ? 0n : feed.bestBidPrice; parts.push(encodeFeedProperty(PROP_ID.BestBidPrice, i64Le(v))); } if (feed.bestAskPrice !== undefined) { const v = feed.bestAskPrice === null ? 0n : feed.bestAskPrice; parts.push(encodeFeedProperty(PROP_ID.BestAskPrice, i64Le(v))); } if (feed.publisherCount !== undefined) { parts.push( encodeFeedProperty(PROP_ID.PublisherCount, u16Le(feed.publisherCount)), ); } if (feed.exponent !== undefined) { parts.push(encodeFeedProperty(PROP_ID.Exponent, i16Le(feed.exponent))); } if (feed.confidence !== undefined) { const v = feed.confidence === null ? 0n : feed.confidence; parts.push(encodeFeedProperty(PROP_ID.Confidence, i64Le(v))); } if (feed.fundingRate !== undefined) { if (feed.fundingRate === null) { parts.push(encodeFeedProperty(PROP_ID.FundingRate, u8(0))); } else { parts.push( encodeFeedProperty( PROP_ID.FundingRate, concat(u8(1), i64Le(feed.fundingRate)), ), ); } } if (feed.fundingTimestamp !== undefined) { if (feed.fundingTimestamp === null) { parts.push(encodeFeedProperty(PROP_ID.FundingTimestamp, u8(0))); } else { parts.push( encodeFeedProperty( PROP_ID.FundingTimestamp, concat(u8(1), u64Le(feed.fundingTimestamp)), ), ); } } if (feed.fundingRateInterval !== undefined) { if (feed.fundingRateInterval === null) { parts.push(encodeFeedProperty(PROP_ID.FundingRateInterval, u8(0))); } else { parts.push( encodeFeedProperty( PROP_ID.FundingRateInterval, concat(u8(1), u64Le(feed.fundingRateInterval)), ), ); } } if (feed.marketSession !== undefined) { const idx = MARKET_SESSION_ORDER.indexOf(feed.marketSession); if (idx < 0) throw new RangeError('Invalid marketSession'); parts.push(encodeFeedProperty(PROP_ID.MarketSession, u16Le(idx))); } if (feed.emaPrice !== undefined) { const v = feed.emaPrice === null ? 0n : feed.emaPrice; parts.push(encodeFeedProperty(PROP_ID.EmaPrice, i64Le(v))); } if (feed.emaConfidence !== undefined) { const v = feed.emaConfidence === null ? 0n : feed.emaConfidence; parts.push(encodeFeedProperty(PROP_ID.EmaConfidence, i64Le(v))); } if (feed.feedUpdateTimestamp !== undefined) { if (feed.feedUpdateTimestamp === null) { parts.push(encodeFeedProperty(PROP_ID.FeedUpdateTimestamp, u8(0))); } else { parts.push( encodeFeedProperty( PROP_ID.FeedUpdateTimestamp, concat(u8(1), u64Le(feed.feedUpdateTimestamp)), ), ); } } const properties = concat(...parts); if (parts.length > 255) throw new RangeError('Feed has more than 255 properties'); return concat( u32Le(feed.feedId), u8(parts.length), // number of properties (parser.repeat count), not byte length properties, ); } /** * Encode a PriceUpdate to the binary payload format consumed by the on-chain parser. * This is the payload that goes inside a PythMessage. */ export function encodePriceUpdate(update: PriceUpdate): Uint8Array { if (update.priceFeeds.length > 255) { throw new RangeError('At most 255 feeds allowed'); } const feeds: Feed[] = update.priceFeeds.map(toFeed); const feedBytes = feeds.map(encodeFeed); return concat( PRICE_UPDATE_MAGIC, u64Le(BigInt(update.timestampUs)), u8(update.channelId), u8(feeds.length), ...feedBytes, ); } /** * Encode a full Pyth message (Solana format): magic + signature + key + payload length + payload. * Use this when you already have a signed payload (e.g. from a Pyth API or after signing locally). */ export function encodePythMessage(parts: PythMessageParts): Uint8Array { if (parts.signature.length !== 64) { throw new RangeError('signature must be 64 bytes'); } if (parts.publicKey.length !== 32) { throw new RangeError('publicKey must be 32 bytes'); } if (parts.payload.length > 0xffff) { throw new RangeError('payload length exceeds u16 max'); } return concat( SOLANA_FORMAT_MAGIC, parts.signature, parts.publicKey, u16Le(parts.payload.length), parts.payload, ); } /** * Encode a PythMessage whose payload is a PriceUpdate. * Signs the payload with the given secret key (32-byte Ed25519 seed). * Returns the full message bytes (hex-friendly). */ export async function encodeSignedPythMessage( update: PriceUpdate, secretKey: Uint8Array, ): Promise<Uint8Array> { if (secretKey.length !== 32) { throw new RangeError('secretKey must be 32 bytes (Ed25519 seed)'); } const noble = await import('@noble/ed25519'); const payload = encodePriceUpdate(update); const publicKey = await noble.getPublicKeyAsync(secretKey); const signature = await noble.signAsync(payload, secretKey); return encodePythMessage({ signature, publicKey, payload }); }