@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
263 lines (244 loc) • 8.48 kB
text/typescript
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 });
}