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