UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

377 lines (361 loc) 12.8 kB
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)); }); });