import Decimal from 'decimal.js'; import BN from 'bn.js'; import { HermesClient } from '@pythnetwork/hermes-client'; import { AccountInfo, ComputeBudgetProgram, Connection, Keypair, PACKET_DATA_SIZE, PublicKey, Signer, SystemProgram, Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; import { HUNDRED_PERCENT_BPS } from '../../constants'; import { OracleSettings } from '../../layouts/oracle'; import { TxBatchData, TxData } from '../../txUtils'; import { HERMES_PUBLIC_ENDPOINT, SOLANA_DEVNET_ENDPOINT } from './constants'; import { OraclePrice } from './oracle'; export type VerificationLevel = | { kind: 'Partial'; numSignatures: number } | { kind: 'Full' }; export function parseVerificationLevel(buf: Buffer, offset: number): { level: VerificationLevel, size: number } { const discr = buf.readUInt8(offset); if (discr === 0) { // Partial const numSignatures = buf.readUInt8(offset + 1); return { level: { kind: 'Partial', numSignatures }, size: 2 }; } else if (discr === 1) { // Full return { level: { kind: 'Full' }, size: 1 }; } else { throw new Error(`Unknown verification level: ${discr}`); } } // Pyth program IDs (mainnet) const DEFAULT_RECEIVER_PROGRAM_ID = new PublicKey("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); const DEFAULT_PUSH_ORACLE_PROGRAM_ID = new PublicKey("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT"); const DEFAULT_WORMHOLE_PROGRAM_ID = new PublicKey("HDwcJBJXjL9FpJ7UBsYBtaDjsBUhuLCUYoz3zr8SWWaQ"); // Constants from Pyth SDK vaa.js const VAA_START = 46; const VAA_SPLIT_INDEX = 721; const VAA_SIGNATURE_SIZE = 66; const DEFAULT_REDUCED_GUARDIAN_SET_SIZE = 5; // Compute budget constants const VERIFY_ENCODED_VAA_COMPUTE_BUDGET = 350_000; const UPDATE_PRICE_FEED_COMPUTE_BUDGET = 55_000; const INIT_ENCODED_VAA_COMPUTE_BUDGET = 3000; const WRITE_ENCODED_VAA_COMPUTE_BUDGET = 3000; const CLOSE_ENCODED_VAA_COMPUTE_BUDGET = 30_000; // Pre-computed discriminators const DISCRIMINATORS = { // Wormhole instructions initEncodedVaa: Buffer.from([209, 193, 173, 25, 91, 202, 181, 218]), writeEncodedVaa: Buffer.from([199, 208, 110, 177, 150, 76, 118, 42]), verifyEncodedVaaV1: Buffer.from([103, 56, 177, 229, 240, 103, 68, 73]), closeEncodedVaa: Buffer.from([48, 221, 174, 198,231, 7, 152, 38]), // Pyth Push Oracle instructions updatePriceFeed: Buffer.from([28, 9, 93, 150, 86, 153, 188, 115]), }; // Jito tip accounts (copied from pyth solana-utils) export const TIP_ACCOUNTS = [ "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", ]; export function getRandomTipAccount(): PublicKey { const idx = Math.floor(Math.random() * TIP_ACCOUNTS.length); return new PublicKey(TIP_ACCOUNTS[idx]); } export function buildJitoTipInstruction(payer: PublicKey, lamports: number): TransactionInstruction { return SystemProgram.transfer({ fromPubkey: payer, toPubkey: getRandomTipAccount(), lamports, }); } /** * Get a random treasury ID (0-255) - same as Pyth SDK's getRandomTreasuryId() */ function getRandomTreasuryId(): number { return Math.floor(Math.random() * 256); } /** * Derives the Pyth price feed account address */ export function getPythPriceFeedAccountAddress( shardId: number, priceFeedId: string | Buffer, pushOracleProgramId: PublicKey = DEFAULT_PUSH_ORACLE_PROGRAM_ID ): PublicKey { let feedIdBuffer: Buffer; if (typeof priceFeedId === "string") { const hexString = priceFeedId.startsWith("0x") ? priceFeedId.slice(2) : priceFeedId; feedIdBuffer = Buffer.from(hexString, "hex"); } else { feedIdBuffer = priceFeedId; } if (feedIdBuffer.length !== 32) { throw new Error("Feed ID should be 32 bytes long"); } const shardBuffer = Buffer.alloc(2); shardBuffer.writeUint16LE(shardId, 0); return PublicKey.findProgramAddressSync( [shardBuffer, feedIdBuffer], pushOracleProgramId )[0]; } /** * Get Treasury PDA for Pyth Receiver */ function getTreasuryPda(treasuryId: number, programId: PublicKey = DEFAULT_RECEIVER_PROGRAM_ID): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("treasury"), Buffer.from([treasuryId])], programId )[0]; } /** * Get Config PDA for Pyth Receiver */ function getConfigPda(programId: PublicKey = DEFAULT_RECEIVER_PROGRAM_ID): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("config")], programId )[0]; } /** * Get Guardian Set PDA for Wormhole */ function getGuardianSetPda(guardianSetIndex: number, programId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID): PublicKey { const indexBuffer = Buffer.alloc(4); indexBuffer.writeUInt32BE(guardianSetIndex, 0); return PublicKey.findProgramAddressSync( [Buffer.from("GuardianSet"), indexBuffer], programId )[0]; } /** * Get guardian set index from VAA (offset 1, big-endian u32) */ function getGuardianSetIndex(vaa: Buffer): number { return vaa.readUInt32BE(1); } /** * Serialize Borsh Vec (4-byte LE length prefix + data) */ function serializeBorshBytes(data: Buffer): Buffer { const lenBuffer = Buffer.alloc(4); lenBuffer.writeUInt32LE(data.length, 0); return Buffer.concat([lenBuffer, data]); } /** * Serialize Borsh Vec<[u8; 20]> for merkle proof */ function serializeBorshProof(proof: Buffer[]): Buffer { const lenBuffer = Buffer.alloc(4); lenBuffer.writeUInt32LE(proof.length, 0); return Buffer.concat([lenBuffer, ...proof]); } export interface InstructionWithSigners { instruction: TransactionInstruction; signers: Keypair[]; computeUnits?: number; } /** * Build initEncodedVaa instruction */ function buildInitEncodedVaaInstruction( writeAuthority: PublicKey, encodedVaa: PublicKey, wormholeProgramId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID ): TransactionInstruction { return new TransactionInstruction({ keys: [ { pubkey: writeAuthority, isSigner: true, isWritable: false }, { pubkey: encodedVaa, isSigner: false, isWritable: true }, ], programId: wormholeProgramId, data: DISCRIMINATORS.initEncodedVaa, }); } /** * Build writeEncodedVaa instruction */ function buildWriteEncodedVaaInstruction( writeAuthority: PublicKey, draftVaa: PublicKey, index: number, data: Buffer, wormholeProgramId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID ): TransactionInstruction { // Serialize args: index (u32 LE) + data (Borsh Vec) const indexBuffer = Buffer.alloc(4); indexBuffer.writeUInt32LE(index, 0); const instructionData = Buffer.concat([ DISCRIMINATORS.writeEncodedVaa, indexBuffer, serializeBorshBytes(data), ]); return new TransactionInstruction({ keys: [ { pubkey: writeAuthority, isSigner: true, isWritable: false }, { pubkey: draftVaa, isSigner: false, isWritable: true }, ], programId: wormholeProgramId, data: instructionData, }); } /** * Build verifyEncodedVaaV1 instruction */ function buildVerifyEncodedVaaV1Instruction( writeAuthority: PublicKey, draftVaa: PublicKey, guardianSetIndex: number, wormholeProgramId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID ): TransactionInstruction { return new TransactionInstruction({ keys: [ { pubkey: writeAuthority, isSigner: true, isWritable: false }, { pubkey: draftVaa, isSigner: false, isWritable: true }, { pubkey: getGuardianSetPda(guardianSetIndex, wormholeProgramId), isSigner: false, isWritable: false }, ], programId: wormholeProgramId, data: DISCRIMINATORS.verifyEncodedVaaV1, }); } /** * Build closeEncodedVaa instruction */ function buildCloseEncodedVaaInstruction( writeAuthority: PublicKey, encodedVaa: PublicKey, wormholeProgramId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID ): TransactionInstruction { return new TransactionInstruction({ keys: [ { pubkey: writeAuthority, isSigner: true, isWritable: true }, { pubkey: encodedVaa, isSigner: false, isWritable: true }, ], programId: wormholeProgramId, data: DISCRIMINATORS.closeEncodedVaa, }); } /** * Build updatePriceFeed instruction for Pyth Push Oracle */ function buildUpdatePriceFeedInstruction(params: { payer: PublicKey; encodedVaa: PublicKey; priceFeedAccount: PublicKey; treasuryId: number; shardId: number; feedId: Buffer; merklePriceUpdate: { message: Buffer; proof: Buffer[] }; receiverProgramId?: PublicKey; pushOracleProgramId?: PublicKey; }): TransactionInstruction { const { payer, encodedVaa, priceFeedAccount, treasuryId, shardId, feedId, merklePriceUpdate, receiverProgramId = DEFAULT_RECEIVER_PROGRAM_ID, pushOracleProgramId = DEFAULT_PUSH_ORACLE_PROGRAM_ID, } = params; const shardBuffer = Buffer.alloc(2); shardBuffer.writeUint16LE(shardId, 0); const data = Buffer.concat([ DISCRIMINATORS.updatePriceFeed, // MerklePriceUpdate serializeBorshBytes(merklePriceUpdate.message), serializeBorshProof(merklePriceUpdate.proof), // treasuryId (u8) Buffer.from([treasuryId]), // shardId (u16 LE) shardBuffer, // feedId ([u8; 32]) feedId, ]); const keys = [ { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: receiverProgramId, isSigner: false, isWritable: false }, { pubkey: encodedVaa, isSigner: false, isWritable: false }, { pubkey: getConfigPda(receiverProgramId), isSigner: false, isWritable: false }, { pubkey: getTreasuryPda(treasuryId, receiverProgramId), isSigner: false, isWritable: true }, { pubkey: priceFeedAccount, isSigner: false, isWritable: true }, { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, ]; return new TransactionInstruction({ keys, programId: pushOracleProgramId, data, }); } export interface PythUpdateResult { postInstructions: InstructionWithSigners[]; closeInstructions: InstructionWithSigners[]; signers: Keypair[]; priceFeedAccounts: Map; } /** * Convert proof from number[][] to Buffer[] */ function convertProof(proof: number[][]): Buffer[] { return proof.map(p => Buffer.from(p)); } export function parsePriceFeedMessage(message: Buffer) { let cursor = 0; const variant = message.readUInt8(cursor); cursor += 1; if (variant !== 0) throw new Error('Not a price feed message'); const feedId = message.subarray(cursor, cursor + 32); cursor += 32; const price = message.subarray(cursor, cursor + 8); cursor += 8; const confidence = message.subarray(cursor, cursor + 8); cursor += 8; const exponent = message.readInt32BE(cursor); cursor += 4; const publishTime = message.subarray(cursor, cursor + 8); cursor += 8; const prevPublishTime = message.subarray(cursor, cursor + 8); cursor += 8; const emaPrice = message.subarray(cursor, cursor + 8); cursor += 8; const emaConf = message.subarray(cursor, cursor + 8); return { feedId, price, confidence, exponent, publishTime, prevPublishTime, emaPrice, emaConf }; } /** parsing of accumulator update data (VAA + updates) */ export function parseAccumulatorUpdateData(data: Buffer): { vaa: Buffer; updates: { message: Buffer; proof: number[][] }[] } { const ACC_MAGIC = '504e4155'; if (data.toString('hex').slice(0, 8) !== ACC_MAGIC || data[4] !== 1 || data[5] !== 0) { throw new Error('Invalid accumulator message'); } let cursor = 6; const trailingPayloadSize = data.readUInt8(cursor); cursor += 1 + trailingPayloadSize; cursor += 1; const vaaSize = data.readUInt16BE(cursor); cursor += 2; const vaa = data.subarray(cursor, cursor + vaaSize); cursor += vaaSize; const numUpdates = data.readUInt8(cursor); cursor += 1; const updates: { message: Buffer; proof: number[][] }[] = []; const HASH_SIZE = 20; for (let i = 0; i < numUpdates; i++) { const messageSize = data.readUInt16BE(cursor); cursor += 2; const message = data.subarray(cursor, cursor + messageSize); cursor += messageSize; const numProofs = data.readUInt8(cursor); cursor += 1; const proof: number[][] = []; for (let j = 0; j < numProofs; j++) { proof.push(Array.from(data.subarray(cursor, cursor + HASH_SIZE))); cursor += HASH_SIZE; } updates.push({ message, proof }); } if (cursor !== data.length) throw new Error("Didn't reach end of message"); return { vaa, updates }; } /** * Get the size of a transaction that would contain the provided array of instructions */ export function getSizeOfTransaction( instructions: TransactionInstruction[], versionedTransaction: boolean = true, addressLookupTable?: { state: { addresses: PublicKey[] } } ): number { const programs = new Set(); const signers = new Set(); let accounts = new Set(); instructions.map((ix) => { programs.add(ix.programId.toBase58()); accounts.add(ix.programId.toBase58()); ix.keys.map((key) => { if (key.isSigner) { signers.add(key.pubkey.toBase58()); } accounts.add(key.pubkey.toBase58()); }); }); const instruction_sizes = instructions .map( (ix) => 1 + getSizeOfCompressedU16(ix.keys.length) + ix.keys.length + getSizeOfCompressedU16(ix.data.length) + ix.data.length ) .reduce((a, b) => a + b, 0); let numberOfAddressLookups = 0; if (addressLookupTable) { const lookupTableAddresses = addressLookupTable.state.addresses.map((address) => address.toBase58()); const totalNumberOfAccounts = accounts.size; accounts = new Set([...accounts].filter((account) => !lookupTableAddresses.includes(account))); accounts = new Set([...accounts, ...programs, ...signers]); numberOfAddressLookups = totalNumberOfAccounts - accounts.size; } return ( getSizeOfCompressedU16(signers.size) + signers.size * 64 + 3 + getSizeOfCompressedU16(accounts.size) + 32 * accounts.size + 32 + getSizeOfCompressedU16(instructions.length) + instruction_sizes + (versionedTransaction ? 1 + getSizeOfCompressedU16(0) : 0) + (versionedTransaction && addressLookupTable ? 32 : 0) + (versionedTransaction && addressLookupTable ? 2 : 0) + numberOfAddressLookups ); } /** Get the size of n in bytes when serialized as a CompressedU16 */ export function getSizeOfCompressedU16(n: number): number { return 1 + Number(n >= 128) + Number(n >= 16384); } const ENCODED_VAA_RENT_EXEMPTION = 7_836_960; export interface InstructionBatch { instructions: TransactionInstruction[]; signers: Keypair[]; computeUnits: number; } /** * Fetch Pyth price accounts from RPC and extract feed IDs */ export async function fetchFeedIdsFromAccounts( connection: Connection, priceAccounts: PublicKey[], ): Promise<{ feedIds: string[], feedIdToAccount: Map }> { const accountInfos = await connection.getMultipleAccountsInfo(priceAccounts, "confirmed"); const feedIds: string[] = []; const feedIdToAccount = new Map(); for (let i = 0; i < priceAccounts.length; i++) { const ai = accountInfos[i]; if (!ai) throw new Error(`Account ${priceAccounts[i].toBase58()} not found`); const [state, _] = PythState.decode(ai.data, 8); const feedIdHex = "0x" + state.priceMessage.feedId.toString("hex"); feedIds.push(feedIdHex); feedIdToAccount.set(feedIdHex, priceAccounts[i]); } return { feedIds, feedIdToAccount }; } export interface PythUpdateTxResult { txBatchData: TxBatchData; /** Keypairs that must sign their respective transactions before sending */ extraSigners: Map; // batchIndex -> signers for tx[0] in that batch } /** * Build Pyth price feed update as TxBatchData compatible with SDK txUtils. * * TxBatchData.batches layout: * batch 0 (sequential): [createAccount + initEncodedVaa + writeVaa_part1] * batch 1 (sequential): [writeVaa_part2 + verifyEncodedVaa] * batch 2 (parallel): [updateFeed_0], [updateFeed_1], ... [updateFeed_N-2] * batch 3 (sequential): [updateFeed_last + closeEncodedVaa] * * Transactions within each batch are sent in parallel by sendVersionedTxs. * Batches are sent sequentially. */ export async function buildPythPriceFeedUpdateIxs( payer: PublicKey, priceFeedIds: string[], defaultShardId?: number, hermesClient?: HermesClient, ): Promise<{ vaaCreateInitEncodeIxs: { ixs: TransactionInstruction[]; signer: Keypair; }[]; vaaWriteVerifyIxs: TransactionInstruction[][]; updateFeedIxs: TransactionInstruction[]; closeVaaIxs: TransactionInstruction[]; }> { const client = hermesClient ?? new HermesClient(HERMES_PUBLIC_ENDPOINT, {}); const shardId = defaultShardId ?? 0; const priceUpdates = await client.getLatestPriceUpdates(priceFeedIds, { encoding: "base64" }); if (!priceUpdates.binary?.data || priceUpdates.binary.data.length === 0) { throw new Error("Failed to fetch price updates from Hermes"); } const treasuryId = getRandomTreasuryId(); let allVaaCreateInitEncodeIxs: { ixs: TransactionInstruction[]; signer: Keypair; }[] = []; let allVaaWriteVerifyIxs: TransactionInstruction[][] = []; const allUpdateIxs: TransactionInstruction[] = []; const allCloseIxs: TransactionInstruction[] = []; for (const updateData of priceUpdates.binary.data) { const accumulatorData = parseAccumulatorUpdateData(Buffer.from(updateData, "base64")); const vaa = accumulatorData.vaa; const encodedVaaKeypair = Keypair.generate(); const encodedVaaSize = vaa.length + VAA_START; const guardianSetIndex = getGuardianSetIndex(vaa); let vaaCreateInitEncodeIxs: TransactionInstruction[] = []; let vaaWriteVerifyIxs: TransactionInstruction[] = []; // createAccount vaaCreateInitEncodeIxs.push( SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: encodedVaaKeypair.publicKey, lamports: ENCODED_VAA_RENT_EXEMPTION, space: encodedVaaSize, programId: DEFAULT_WORMHOLE_PROGRAM_ID, }) ); // initEncodedVaa vaaCreateInitEncodeIxs.push( buildInitEncodedVaaInstruction(payer, encodedVaaKeypair.publicKey) ); // writeEncodedVaa part 1 const firstPartEnd = Math.min(VAA_SPLIT_INDEX, vaa.length); vaaCreateInitEncodeIxs.push( buildWriteEncodedVaaInstruction(payer, encodedVaaKeypair.publicKey, 0, vaa.subarray(0, firstPartEnd)) ); // writeEncodedVaa part 2 (if needed) if (vaa.length > VAA_SPLIT_INDEX) { vaaWriteVerifyIxs.push( buildWriteEncodedVaaInstruction(payer, encodedVaaKeypair.publicKey, VAA_SPLIT_INDEX, vaa.subarray(VAA_SPLIT_INDEX)) ); } // verifyEncodedVaaV1 vaaWriteVerifyIxs.push( buildVerifyEncodedVaaV1Instruction(payer, encodedVaaKeypair.publicKey, guardianSetIndex) ); // updateFeed per price update for (const update of accumulatorData.updates) { const parsedMessage = parsePriceFeedMessage(update.message); const feedId = parsedMessage.feedId; allUpdateIxs.push( buildUpdatePriceFeedInstruction({ payer, encodedVaa: encodedVaaKeypair.publicKey, priceFeedAccount: getPythPriceFeedAccountAddress(shardId, feedId), treasuryId, shardId, feedId, merklePriceUpdate: { message: update.message, proof: convertProof(update.proof), }, }), ); } allVaaCreateInitEncodeIxs.push({ ixs: vaaCreateInitEncodeIxs, signer: encodedVaaKeypair, }); allVaaWriteVerifyIxs.push(vaaWriteVerifyIxs); // closeEncodedVaa allCloseIxs.push( buildCloseEncodedVaaInstruction(payer, encodedVaaKeypair.publicKey), ); } return { vaaCreateInitEncodeIxs: allVaaCreateInitEncodeIxs, vaaWriteVerifyIxs: allVaaWriteVerifyIxs, updateFeedIxs: allUpdateIxs, closeVaaIxs: allCloseIxs, }; } export class PriceFeedMessage { feedId: Buffer; price: BN; // i64 (signed) conf: BN; // u64 exponent: number; // i32 publishTime: BN; // i64 prevPublishTime: BN; // i64 emaPrice: BN; // i64 emaConf: BN; // u64 constructor(params: { feedId: Buffer, price: BN, conf: BN, exponent: number, publishTime: BN, prevPublishTime: BN, emaPrice: BN, emaConf: BN }) { this.feedId = params.feedId; this.price = params.price; this.conf = params.conf; this.exponent = params.exponent; this.publishTime = params.publishTime; this.prevPublishTime = params.prevPublishTime; this.emaPrice = params.emaPrice; this.emaConf = params.emaConf; } static decode(buf: Buffer, offset: number = 0): [PriceFeedMessage, number] { let cursor = offset; // feed_id: [u8;32] const feedId = buf.subarray(cursor, cursor + 32); cursor += 32; // price: i64 (signed) const price = new BN(buf.subarray(cursor, cursor + 8), "le").fromTwos(64); cursor += 8; // conf: u64 (unsigned) const conf = new BN(buf.subarray(cursor, cursor + 8), "le"); cursor += 8; // exponent: i32 (signed) const exponent = new BN(buf.subarray(cursor, cursor + 4), "le").fromTwos(32).toNumber(); cursor += 4; // publish_time: i64 (signed) const publishTime = new BN(buf.subarray(cursor, cursor + 8), "le").fromTwos(64); cursor += 8; // prev_publish_time: i64 (signed) const prevPublishTime = new BN(buf.subarray(cursor, cursor + 8), "le").fromTwos(64); cursor += 8; // ema_price: i64 (signed) const emaPrice = new BN(buf.subarray(cursor, cursor + 8), "le").fromTwos(64); cursor += 8; // ema_conf: u64 (unsigned) const emaConf = new BN(buf.subarray(cursor, cursor + 8), "le"); cursor += 8; return [ new PriceFeedMessage({ feedId, price, conf, exponent, publishTime, prevPublishTime, emaPrice, emaConf }), cursor ]; } } export class PythState { writeAuthority: PublicKey; verificationLevel: VerificationLevel; priceMessage: PriceFeedMessage; postedSlot: BN; constructor( writeAuthority: PublicKey, verificationLevel: VerificationLevel, priceMessage: PriceFeedMessage, postedSlot: BN ) { this.writeAuthority = writeAuthority; this.verificationLevel = verificationLevel; this.priceMessage = priceMessage; this.postedSlot = postedSlot; }; static decode(buf: Buffer, offset: number = 0): [PythState, number] { let cursor = offset; // write_authority: Pubkey (32) const writeAuthority = new PublicKey(buf.subarray(cursor, cursor + 32)); cursor += 32; const { level, size } = parseVerificationLevel(buf, cursor); cursor += size; // price_message: PriceFeedMessage const [priceMessage, next] = PriceFeedMessage.decode(buf, cursor); cursor = next; // posted_slot: u64 const postedSlot = new BN(buf.subarray(cursor, cursor + 8), "le"); cursor += 8; return [ new PythState(writeAuthority, level, priceMessage, postedSlot), cursor ]; } } export class PythOracle { static fetch( oracleParams: OracleSettings, accountInfos: AccountInfo[], solPrice?: OraclePrice, usdPrice?: OraclePrice, ): OraclePrice { //@ts-ignore let state: PythState = null; try { const stateAi = accountInfos[0]; const [parsedState, _] = PythState.decode(stateAi.data, 8); state = parsedState; } catch (error) { return new OraclePrice(new Decimal(0), new Decimal(0), 0); } let pr = new Decimal(state.priceMessage.price.toString()).mul(Decimal.pow(10, state.priceMessage.exponent - oracleParams.tokenDecimals)); let ema = new Decimal(state.priceMessage.emaPrice.toString()).mul(Decimal.pow(10, state.priceMessage.exponent - oracleParams.tokenDecimals)); let cf = new Decimal(state.priceMessage.conf.toString()).mul(Decimal.pow(10, state.priceMessage.exponent - oracleParams.tokenDecimals)); const lastUpdateTimestamp = state.priceMessage.publishTime; // Validate primary price is not zero if (pr.lte(0)) { return new OraclePrice(new Decimal(0), new Decimal(0), 0); } // === 1. Inflate confidence by staleness === // confidence = confidence * (1 + delta_t * stalenessConfRateBps / 10_000) const now: BN = new BN(Math.floor(Date.now() / 1000)); const deltaSeconds = Math.max(now.toNumber() - lastUpdateTimestamp.toNumber(), 0); const deltaT = new Decimal(deltaSeconds); const deltaTBN = new BN(deltaSeconds); const stalenessRate = new Decimal(oracleParams.stalenessConfRateBps).div(new Decimal(HUNDRED_PERCENT_BPS)); const inflateFactor = new Decimal(1).add(deltaT.mul(stalenessRate)); cf = cf.mul(inflateFactor); let validated: boolean = true; // === 2. Validate confidence threshold === // confidence / price * 10_000 < confThreshBps const confRatioBps = cf.div(pr).mul(new Decimal(HUNDRED_PERCENT_BPS)); if (confRatioBps.gt(new Decimal(oracleParams.confThreshBps))) validated = false; // === 3. Validate staleness threshold === if (deltaTBN.gt(oracleParams.stalenessThresh)) validated = false; // === 4. Validate volatility threshold using EMA and price === // max(ema, price) / min(ema, price) * 10_000 < volatilityThreshBps const maxPrice = Decimal.max(ema, pr); const minPrice = Decimal.min(ema, pr); if (!minPrice.eq(0)) { const volRatio = maxPrice.sub(minPrice).div(minPrice); const volRatioBps = volRatio.mul(new Decimal(HUNDRED_PERCENT_BPS)); if (volRatioBps.gt(new Decimal(oracleParams.volatilityThreshBps))) { validated = false; } } else { validated = false; } // we don't validate liquidity for Pyth return new OraclePrice( pr, cf, parseInt(state.priceMessage.publishTime.toString()), validated ); } // async fetchPriceFromHermes(priceFeedId: string): Promise { // const priceUpdate = await this.priceClient.getLatestPriceUpdates([priceFeedId], {encoding: "base64"}); // if(priceUpdate && priceUpdate.parsed && priceUpdate.parsed.length > 0) { // const priceData = priceUpdate.parsed[0].price; // return { // price: new BN(priceData.price), // conf: new BN(priceData.conf), // expo: priceData.expo, // publishTime: new BN(priceData.publishTime) // }; // } else { // throw new Error(`Failed to fetch price update from Hermes for ${priceFeedId}`); // } // } // /** // * Should set feedId before running this // * @param {number} shardId - between 0-2^64, default is 0 // * @param {number} [computeUnitPrice] - compute unit price in microlamports, default is 100_000 // */ // async updateFeedTx(wallet: any, shardId: number = 0, computeUnitPrice: number = 100000) { // if(!shardId) shardId = 0; // if(!computeUnitPrice) computeUnitPrice = 100000; // let priceUpdate = await this.priceClient.getLatestPriceUpdates([this.priceFeedId], {encoding: "base64"}); // const pythSolanaReceiver = new PythSolanaReceiver({ // connection: this.connection, // wallet: wallet, // }); // const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({ // // closeUpdateAccounts: false // }); // await transactionBuilder.addUpdatePriceFeed(priceUpdate.binary.data, shardId); // let build = await transactionBuilder.buildVersionedTransactions({ // computeUnitPriceMicroLamports: computeUnitPrice, // }); // return build; // }; // async sendUpdateTx( // provider: AnchorProvider, // updateFeedTx: { // tx: VersionedTransaction; // signers: Signer[]; // }[] // ): Promise{ // let txs = await provider.sendAll(updateFeedTx, { skipPreflight: true, commitment: "confirmed"}); // return txs; // } }