@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
377 lines (361 loc) • 12.8 kB
text/typescript
import { describe, expect, test } from 'vitest';
import {
decodePriceUpdate,
decodePythMessage,
encodePriceUpdate,
encodePythMessage,
encodeSignedPythMessage,
PRICE_UPDATE_MAGIC,
SOLANA_FORMAT_MAGIC,
type PriceUpdate,
type PythMessageParts,
} from '../../src/utils/pyth';
/** Generate a 32-byte Ed25519 secret key for tests. */
function randomSecretKey(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(32));
}
describe('Pyth > Encoding', () => {
test('encodePriceUpdate uses Aiken price_update format (magic + timestamp + channel u8 + feeds_len u8 + property-list feeds)', () => {
const timestampUs = 1000000n;
const update: PriceUpdate = {
timestampUs: timestampUs.toString(),
channelId: 0,
priceFeeds: [
{
priceFeedId: 1,
exponent: -8,
price: '50000000',
feedUpdateTimestamp: Number(timestampUs),
},
{
priceFeedId: 2,
exponent: 0,
price: '-100',
feedUpdateTimestamp: Number(timestampUs + 1n),
},
],
};
const payload = encodePriceUpdate(update);
expect(Array.from(payload.subarray(0, 4))).toEqual(
Array.from(PRICE_UPDATE_MAGIC),
);
// magic(4) + timestamp(8) + channel(1) + feeds_len(1) + 2 feeds
// For this particular update, each feed encodes 13 properties:
// Price, BestBidPrice, BestAskPrice, PublisherCount, Exponent, Confidence,
// FundingRate, FundingTimestamp, FundingRateInterval, MarketSession,
// EmaPrice, EmaConfidence, FeedUpdateTimestamp.
// One feed layout:
// - feed_id u32 (4)
// - properties_len u8 (1)
// - properties bytes:
// Price: 1 (id) + 8 (i64)
// BestBidPrice: 1 (id) + 8 (i64)
// BestAskPrice: 1 (id) + 8 (i64)
// PublisherCount: 1 (id) + 2 (u16)
// Exponent: 1 (id) + 2 (i16)
// Confidence: 1 (id) + 8 (i64)
// FundingRate: 1 (id) + 1 (none tag)
// FundingTimestamp: 1 (id) + 1 (none tag)
// FundingRateInterval:1 (id) + 1 (none tag)
// MarketSession: 1 (id) + 2 (u16)
// EmaPrice: 1 (id) + 8 (i64)
// EmaConfidence: 1 (id) + 8 (i64)
// FeedUpdateTimestamp:1 (id) + 1 (some tag) + 8 (u64)
const feedBytes =
4 + // feed_id
1 + // properties_len
(1 + 8) + // Price
(1 + 8) + // BestBidPrice
(1 + 8) + // BestAskPrice
(1 + 2) + // PublisherCount
(1 + 2) + // Exponent
(1 + 8) + // Confidence
(1 + 1) + // FundingRate (None)
(1 + 1) + // FundingTimestamp (None)
(1 + 1) + // FundingRateInterval (None)
(1 + 2) + // MarketSession
(1 + 8) + // EmaPrice
(1 + 8) + // EmaConfidence
(1 + 1 + 8); // FeedUpdateTimestamp (Some + u64)
expect(payload.length).toBe(4 + 8 + 1 + 1 + 2 * feedBytes);
});
test('encodeSignedPythMessage returns Pyth wire format (magic + signature + key + size + payload)', async () => {
const secretKey = randomSecretKey();
const timestampUs = 2000000n;
const update: PriceUpdate = {
timestampUs: timestampUs.toString(),
channelId: 0,
priceFeeds: [
{
priceFeedId: 42,
exponent: -9,
price: '12345',
feedUpdateTimestamp: Number(timestampUs),
},
],
};
const message = await encodeSignedPythMessage(update, secretKey);
const payload = encodePriceUpdate(update);
expect(message.length).toBe(4 + 64 + 32 + 2 + payload.length);
expect(Array.from(message.subarray(0, 4))).toEqual(
Array.from(SOLANA_FORMAT_MAGIC),
);
});
test('encodePythMessage produces correct layout (magic + sig + key + u16 len + payload)', async () => {
const secretKey = randomSecretKey();
const noble = await import('@noble/ed25519');
const publicKey = await noble.getPublicKeyAsync(secretKey);
const timestampUs = 1n;
const update: PriceUpdate = {
timestampUs: timestampUs.toString(),
channelId: 0,
priceFeeds: [
{
priceFeedId: 0,
exponent: 0,
price: '0',
feedUpdateTimestamp: Number(timestampUs),
},
],
};
const payload = encodePriceUpdate(update);
const signature = await noble.signAsync(payload, secretKey);
const parts: PythMessageParts = { signature, publicKey, payload };
const message = encodePythMessage(parts);
expect(message.length).toBe(4 + 64 + 32 + 2 + payload.length);
expect(Array.from(message.subarray(0, 4))).toEqual(
Array.from(SOLANA_FORMAT_MAGIC),
);
});
test('encodePriceUpdate supports full Feed properties', () => {
const timestampUs = 1772613309000000n;
const update: PriceUpdate = {
timestampUs: timestampUs.toString(),
channelId: 3,
priceFeeds: [
{
priceFeedId: 1,
price: '6950870030930',
bestBidPrice: '6950841224625',
bestAskPrice: '6950918797952',
exponent: -8,
marketSession: 'Regular',
emaPrice: '6891705600000',
emaConfidence: 6891641300000,
feedUpdateTimestamp: Number(timestampUs),
},
],
};
const payload = encodePriceUpdate(update);
expect(Array.from(payload.subarray(0, 4))).toEqual(
Array.from(PRICE_UPDATE_MAGIC),
);
expect(payload.length).toBeGreaterThan(4 + 8 + 1 + 1);
});
// Encoding counterpart of Aiken parses_update() test.
// Same subscription: priceFeedIds [1, 2, 112], properties [price, bestBidPrice, bestAskPrice,
// exponent, fundingRate, fundingTimestamp, fundingRateInterval, marketSession, emaPrice,
// emaConfidence, feedUpdateTimestamp], channel fixed_rate@200ms → channel_id 3.
// Parsed values from streamUpdated.timestampUs and priceFeeds.
test('encodePriceUpdate matches parses_update subscription (three feeds, full properties)', () => {
const timestampUs = 1772613309000000n;
const update: PriceUpdate = {
timestampUs: timestampUs.toString(),
channelId: 3,
priceFeeds: [
{
priceFeedId: 1,
price: '6950870030930',
bestBidPrice: '6950841224625',
bestAskPrice: '6950918797952',
exponent: -8,
marketSession: 'Regular',
emaPrice: '6891705600000',
emaConfidence: 6891641300000,
feedUpdateTimestamp: Number(timestampUs),
},
{
priceFeedId: 2,
price: '201300331620',
bestBidPrice: '201299371656',
bestAskPrice: '201307527050',
exponent: -8,
marketSession: 'Regular',
emaPrice: '199516364000',
emaConfidence: 199513381000,
feedUpdateTimestamp: Number(timestampUs),
},
{
priceFeedId: 112,
price: '6946927951449',
exponent: -12,
fundingRate: 2520,
fundingTimestamp: 1772613308916265,
fundingRateInterval: 28800000000,
marketSession: 'Regular',
feedUpdateTimestamp: Number(timestampUs),
},
],
};
const payload = encodePriceUpdate(update);
expect(Array.from(payload.subarray(0, 4))).toEqual(
Array.from(PRICE_UPDATE_MAGIC),
);
const view = new DataView(
payload.buffer,
payload.byteOffset,
payload.byteLength,
);
const encodedTimestamp = view.getBigUint64(4, true);
expect(encodedTimestamp).toBe(timestampUs);
expect(payload[12]).toBe(3); // channel_id
expect(payload[13]).toBe(3); // feeds_len
// Feed IDs (u32 LE) appear in payload: 1 = 01 00 00 00, 2 = 02 00 00 00, 112 = 70 00 00 00
const payloadBytes = Array.from(payload);
const feed1Le = [1, 0, 0, 0];
const feed2Le = [2, 0, 0, 0];
const feed112Le = [112, 0, 0, 0];
const findU32Le = (arr: number[], val: number[]) => {
for (let i = 0; i <= arr.length - 4; i++) {
if (
arr[i] === val[0] &&
arr[i + 1] === val[1] &&
arr[i + 2] === val[2] &&
arr[i + 3] === val[3]
)
return i;
}
return -1;
};
const idx1 = findU32Le(payloadBytes, feed1Le);
const idx2 = findU32Le(payloadBytes, feed2Le);
const idx112 = findU32Le(payloadBytes, feed112Le);
expect(idx1).toBeGreaterThanOrEqual(14);
expect(idx2).toBeGreaterThan(idx1);
expect(idx112).toBeGreaterThan(idx2);
});
});
describe('Pyth > Decoding', () => {
test('decodePythMessage recovers parts from encodePythMessage', async () => {
const secretKey = randomSecretKey();
const noble = await import('@noble/ed25519');
const publicKey = await noble.getPublicKeyAsync(secretKey);
const timestampUs = 3000000n;
const update: PriceUpdate = {
timestampUs: timestampUs.toString(),
channelId: 1,
priceFeeds: [
{
priceFeedId: 99,
exponent: -6,
price: '42',
feedUpdateTimestamp: Number(timestampUs),
},
],
};
const payload = encodePriceUpdate(update);
const signature = await noble.signAsync(payload, secretKey);
const parts: PythMessageParts = { signature, publicKey, payload };
const message = encodePythMessage(parts);
const decoded = decodePythMessage(message);
expect(decoded.payload.length).toBe(payload.length);
expect(Array.from(decoded.signature)).toEqual(Array.from(signature));
expect(Array.from(decoded.publicKey)).toEqual(Array.from(publicKey));
expect(Array.from(decoded.payload)).toEqual(Array.from(payload));
});
test('decodePriceUpdate round-trips encodePriceUpdate (binary identity)', () => {
const timestampUs = 1000000n;
const update: PriceUpdate = {
timestampUs: timestampUs.toString(),
channelId: 0,
priceFeeds: [
{
priceFeedId: 1,
exponent: -8,
price: '50000000',
feedUpdateTimestamp: Number(timestampUs),
},
{
priceFeedId: 2,
exponent: 0,
price: '-100',
feedUpdateTimestamp: Number(timestampUs + 1n),
},
],
};
const encoded = encodePriceUpdate(update);
const decoded = decodePriceUpdate(encoded);
const again = encodePriceUpdate(decoded);
expect(Array.from(again)).toEqual(Array.from(encoded));
});
test('decodePriceUpdate round-trips parses_update-style subscription payload', () => {
const timestampUs = 1772613309000000n;
const update: PriceUpdate = {
timestampUs: timestampUs.toString(),
channelId: 3,
priceFeeds: [
{
priceFeedId: 1,
price: '6950870030930',
bestBidPrice: '6950841224625',
bestAskPrice: '6950918797952',
exponent: -8,
marketSession: 'Regular',
emaPrice: '6891705600000',
emaConfidence: 6891641300000,
feedUpdateTimestamp: Number(timestampUs),
},
{
priceFeedId: 2,
price: '201300331620',
bestBidPrice: '201299371656',
bestAskPrice: '201307527050',
exponent: -8,
marketSession: 'Regular',
emaPrice: '199516364000',
emaConfidence: 199513381000,
feedUpdateTimestamp: Number(timestampUs),
},
{
priceFeedId: 112,
price: '6946927951449',
exponent: -12,
fundingRate: 2520,
fundingTimestamp: 1772613308916265,
fundingRateInterval: 28800000000,
marketSession: 'Regular',
feedUpdateTimestamp: Number(timestampUs),
},
],
};
const encoded = encodePriceUpdate(update);
const decoded = decodePriceUpdate(encoded);
expect(decoded.timestampUs).toBe(update.timestampUs);
expect(decoded.channelId).toBe(update.channelId);
expect(decoded.priceFeeds.length).toBe(3);
const again = encodePriceUpdate(decoded);
expect(Array.from(again)).toEqual(Array.from(encoded));
});
test('encodeSignedPythMessage wire: decodePythMessage payload round-trips through decodePriceUpdate', async () => {
const secretKey = randomSecretKey();
const timestampUs = 4000000n;
const update: PriceUpdate = {
timestampUs: timestampUs.toString(),
channelId: 0,
priceFeeds: [
{
priceFeedId: 7,
exponent: -4,
price: '1',
feedUpdateTimestamp: Number(timestampUs),
},
],
};
const wire = await encodeSignedPythMessage(update, secretKey);
const parts = decodePythMessage(wire);
const payloadRoundTrip = encodePriceUpdate(
decodePriceUpdate(parts.payload),
);
expect(Array.from(payloadRoundTrip)).toEqual(Array.from(parts.payload));
});
});