diff --git a/apps/price_pusher/package.json b/apps/price_pusher/package.json index 3a858803f3..277f901caa 100644 --- a/apps/price_pusher/package.json +++ b/apps/price_pusher/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/price-pusher", - "version": "9.0.0", + "version": "9.0.1", "description": "Pyth Price Pusher", "homepage": "https://pyth.network", "main": "lib/index.js", diff --git a/apps/price_pusher/src/solana/command.ts b/apps/price_pusher/src/solana/command.ts index 12c359169f..f0077ffbdd 100644 --- a/apps/price_pusher/src/solana/command.ts +++ b/apps/price_pusher/src/solana/command.ts @@ -81,6 +81,17 @@ export default { type: "number", default: 6, } as Options, + "address-lookup-table-account": { + description: "The pubkey of the ALT to use when updating price feeds", + type: "string", + optional: true, + } as Options, + "treasury-id": { + description: + "The treasuryId to use. Useful when the corresponding treasury account is indexed in the ALT passed to --address-lookup-table-account. This is a tx size optimization and is optional; if not set, a random treasury account will be used.", + type: "number", + optional: true, + } as Options, ...options.priceConfigFile, ...options.priceServiceEndpoint, ...options.pythContractAddress, @@ -107,6 +118,8 @@ export default { maxJitoTipLamports, jitoBundleSize, updatesPerJitoBundle, + addressLookupTableAccount, + treasuryId, logLevel, controllerLogLevel, } = argv; @@ -145,12 +158,21 @@ export default { ) ); + const connection = new Connection(endpoint, "processed"); const pythSolanaReceiver = new PythSolanaReceiver({ - connection: new Connection(endpoint, "processed"), + connection, wallet, pushOracleProgramId: new PublicKey(pythContractAddress), + treasuryId: treasuryId, }); + // Fetch the account lookup table if provided + const lookupTableAccount = addressLookupTableAccount + ? await connection + .getAddressLookupTable(new PublicKey(addressLookupTableAccount)) + .then((result) => result.value ?? undefined) + : undefined; + let solanaPricePusher; if (jitoTipLamports) { const jitoKeypair = Keypair.fromSecretKey( @@ -168,7 +190,8 @@ export default { maxJitoTipLamports, jitoClient, jitoBundleSize, - updatesPerJitoBundle + updatesPerJitoBundle, + lookupTableAccount ); onBundleResult(jitoClient, logger.child({ module: "JitoClient" })); @@ -178,7 +201,8 @@ export default { hermesClient, logger.child({ module: "SolanaPricePusher" }), shardId, - computeUnitPriceMicroLamports + computeUnitPriceMicroLamports, + lookupTableAccount ); } diff --git a/apps/price_pusher/src/solana/solana.ts b/apps/price_pusher/src/solana/solana.ts index 668c46c128..32256b7691 100644 --- a/apps/price_pusher/src/solana/solana.ts +++ b/apps/price_pusher/src/solana/solana.ts @@ -14,7 +14,7 @@ import { import { SearcherClient } from "jito-ts/dist/sdk/block-engine/searcher"; import { sliceAccumulatorUpdateData } from "@pythnetwork/price-service-sdk"; import { Logger } from "pino"; -import { LAMPORTS_PER_SOL } from "@solana/web3.js"; +import { AddressLookupTableAccount, LAMPORTS_PER_SOL } from "@solana/web3.js"; const HEALTH_CHECK_TIMEOUT_SECONDS = 60; @@ -97,7 +97,8 @@ export class SolanaPricePusher implements IPricePusher { private hermesClient: HermesClient, private logger: Logger, private shardId: number, - private computeUnitPriceMicroLamports: number + private computeUnitPriceMicroLamports: number, + private addressLookupTableAccount?: AddressLookupTableAccount ) {} async updatePriceFeed(priceIds: string[]): Promise { @@ -126,9 +127,12 @@ export class SolanaPricePusher implements IPricePusher { return; } - const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder({ - closeUpdateAccounts: true, - }); + const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder( + { + closeUpdateAccounts: true, + }, + this.addressLookupTableAccount + ); await transactionBuilder.addUpdatePriceFeed( priceFeedUpdateData, this.shardId @@ -164,7 +168,8 @@ export class SolanaPricePusherJito implements IPricePusher { private maxJitoTipLamports: number, private searcherClient: SearcherClient, private jitoBundleSize: number, - private updatesPerJitoBundle: number + private updatesPerJitoBundle: number, + private addressLookupTableAccount?: AddressLookupTableAccount ) {} async getRecentJitoTipLamports(): Promise { @@ -215,9 +220,12 @@ export class SolanaPricePusherJito implements IPricePusher { } for (let i = 0; i < priceIds.length; i += this.updatesPerJitoBundle) { - const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder({ - closeUpdateAccounts: true, - }); + const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder( + { + closeUpdateAccounts: true, + }, + this.addressLookupTableAccount + ); await transactionBuilder.addUpdatePriceFeed( priceFeedUpdateData.map((x) => { return sliceAccumulatorUpdateData( diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts index e4153ce321..474f8389dc 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts @@ -26,7 +26,18 @@ async function main() { `Sending transactions from account: ${keypair.publicKey.toBase58()}` ); const wallet = new Wallet(keypair); - const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet }); + // Optionally use an account lookup table to reduce tx sizes. + const addressLookupTableAccount = new PublicKey( + "5DNCErWQFBdvCxWQXaC1mrEFsvL3ftrzZ2gVZWNybaSX" + ); + // Use a stable treasury ID of 0, since its address is indexed in the address lookup table. + // This is a tx size optimization and is optional. If not provided, a random treasury account will be used. + const treasuryId = 1; + const pythSolanaReceiver = new PythSolanaReceiver({ + connection, + wallet, + treasuryId, + }); // Get the price update from hermes const priceUpdateData = await getPriceUpdateData(); @@ -35,9 +46,15 @@ async function main() { // If closeUpdateAccounts = true, the builder will automatically generate instructions to close the ephemeral price update accounts // at the end of the transaction. Closing the accounts will reclaim their rent. // The example is using closeUpdateAccounts = false so you can easily look up the price update account in an explorer. - const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({ - closeUpdateAccounts: false, - }); + const lookupTableAccount = + (await connection.getAddressLookupTable(addressLookupTableAccount)).value ?? + undefined; + const transactionBuilder = pythSolanaReceiver.newTransactionBuilder( + { + closeUpdateAccounts: false, + }, + lookupTableAccount + ); // Post the price updates to ephemeral accounts, one per price feed. await transactionBuilder.addPostPriceUpdates(priceUpdateData); console.log( diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts index aee3ae6d29..5012f41960 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts @@ -11,6 +11,7 @@ const SOL_PRICE_FEED_ID = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; const ETH_PRICE_FEED_ID = "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; +const PRICE_FEED_IDS = [SOL_PRICE_FEED_ID, ETH_PRICE_FEED_ID]; let keypairFile = ""; if (process.env["SOLANA_KEYPAIR"]) { @@ -26,24 +27,45 @@ async function main() { `Sending transactions from account: ${keypair.publicKey.toBase58()}` ); const wallet = new Wallet(keypair); - const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet }); + + // Optionally use an account lookup table to reduce tx sizes. + const addressLookupTableAccount = new PublicKey( + "5DNCErWQFBdvCxWQXaC1mrEFsvL3ftrzZ2gVZWNybaSX" + ); + // Use a stable treasury ID of 0, since its address is indexed in the address lookup table. + // This is a tx size optimization and is optional. If not provided, a random treasury account will be used. + const treasuryId = 1; + const pythSolanaReceiver = new PythSolanaReceiver({ + connection, + wallet, + treasuryId, + }); // Get the price update from hermes - const priceUpdateData = await getPriceUpdateData(); + const priceUpdateData = await getPriceUpdateData(PRICE_FEED_IDS); console.log(`Posting price update: ${priceUpdateData}`); // The shard indicates which set of price feed accounts you wish to update. const shardId = 1; + const lookupTableAccount = + (await connection.getAddressLookupTable(addressLookupTableAccount)).value ?? + undefined; + const transactionBuilder = pythSolanaReceiver.newTransactionBuilder( + {}, + lookupTableAccount + ); - const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({}); // Update the price feed accounts for the feed ids in priceUpdateData (in this example, SOL and ETH) and shard id. await transactionBuilder.addUpdatePriceFeed(priceUpdateData, shardId); - console.log( - "The SOL/USD price update will get posted to:", - pythSolanaReceiver - .getPriceFeedAccountAddress(shardId, SOL_PRICE_FEED_ID) - .toBase58() - ); + // Print all price feed accounts that will be updated + for (const priceFeedId of PRICE_FEED_IDS) { + console.log( + `The ${priceFeedId} price update will get posted to:`, + pythSolanaReceiver + .getPriceFeedAccountAddress(shardId, priceFeedId) + .toBase58() + ); + } await transactionBuilder.addPriceConsumerInstructions( async ( @@ -69,16 +91,12 @@ async function main() { } // Fetch price update data from Hermes -async function getPriceUpdateData() { - const priceServiceConnection = new HermesClient( - "https://hermes.pyth.network/", - {} - ); +async function getPriceUpdateData(price_feed_ids: string[]) { + const hermesClient = new HermesClient("https://hermes.pyth.network/", {}); - const response = await priceServiceConnection.getLatestPriceUpdates( - [SOL_PRICE_FEED_ID, ETH_PRICE_FEED_ID], - { encoding: "base64" } - ); + const response = await hermesClient.getLatestPriceUpdates(price_feed_ids, { + encoding: "base64", + }); return response.binary.data; } diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/package.json b/target_chains/solana/sdk/js/pyth_solana_receiver/package.json index ff826b26f4..7374c7833e 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/package.json +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-solana-receiver", - "version": "0.9.1", + "version": "0.10.0", "description": "Pyth solana receiver SDK", "homepage": "https://pyth.network", "main": "lib/index.js", diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts index c6f4bd88e2..3762b022e0 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts @@ -66,6 +66,12 @@ export type PythTransactionBuilderConfig = { closeUpdateAccounts?: boolean; }; +/** + * A stable treasury ID. This ID's corresponding treasury address + * can be cached in an account lookup table in order to reduce the overall txn size. + */ +export const DEFAULT_TREASURY_ID = 0; + /** * A builder class to build transactions that: * - Post price updates (fully or partially verified) or update price feed accounts @@ -430,20 +436,29 @@ export class PythSolanaReceiver { readonly receiver: Program; readonly wormhole: Program; readonly pushOracle: Program; - + readonly treasuryId?: number; constructor({ connection, wallet, wormholeProgramId = DEFAULT_WORMHOLE_PROGRAM_ID, receiverProgramId = DEFAULT_RECEIVER_PROGRAM_ID, pushOracleProgramId = DEFAULT_PUSH_ORACLE_PROGRAM_ID, + treasuryId = undefined, }: { connection: Connection; wallet: Wallet; wormholeProgramId?: PublicKey; receiverProgramId?: PublicKey; pushOracleProgramId?: PublicKey; + // Optionally provide a treasuryId to always use a specific treasury account. + // This can be useful when using an ALT to reduce tx size. + // If not provided, treasury accounts will be randomly selected. + treasuryId?: number; }) { + if (treasuryId !== undefined && (treasuryId < 0 || treasuryId > 255)) { + throw new Error("treasuryId must be between 0 and 255"); + } + this.connection = connection; this.wallet = wallet; this.provider = new AnchorProvider(this.connection, this.wallet, { @@ -464,15 +479,17 @@ export class PythSolanaReceiver { pushOracleProgramId, this.provider ); + this.treasuryId = treasuryId; } /** * Get a new transaction builder to build transactions that interact with the Pyth Solana Receiver program and consume price updates */ newTransactionBuilder( - config: PythTransactionBuilderConfig + config: PythTransactionBuilderConfig, + addressLookupAccount?: AddressLookupTableAccount ): PythTransactionBuilder { - return new PythTransactionBuilder(this, config); + return new PythTransactionBuilder(this, config, addressLookupAccount); } /** @@ -497,7 +514,7 @@ export class PythSolanaReceiver { const priceFeedIdToPriceUpdateAccount: Record = {}; const closeInstructions: InstructionWithEphemeralSigners[] = []; - const treasuryId = getRandomTreasuryId(); + const treasuryId = this.treasuryId ?? getRandomTreasuryId(); for (const priceUpdateData of priceUpdateDataArray) { const accumulatorUpdateData = parseAccumulatorUpdateData( @@ -565,7 +582,7 @@ export class PythSolanaReceiver { const priceFeedIdToPriceUpdateAccount: Record = {}; const closeInstructions: InstructionWithEphemeralSigners[] = []; - const treasuryId = getRandomTreasuryId(); + const treasuryId = this.treasuryId ?? getRandomTreasuryId(); for (const priceUpdateData of priceUpdateDataArray) { const accumulatorUpdateData = parseAccumulatorUpdateData( @@ -636,7 +653,7 @@ export class PythSolanaReceiver { const priceFeedIdToTwapUpdateAccount: Record = {}; const closeInstructions: InstructionWithEphemeralSigners[] = []; - const treasuryId = getRandomTreasuryId(); + const treasuryId = this.treasuryId ?? getRandomTreasuryId(); if (twapUpdateDataArray.length !== 2) { throw new Error( @@ -730,7 +747,7 @@ export class PythSolanaReceiver { const priceFeedIdToPriceUpdateAccount: Record = {}; const closeInstructions: InstructionWithEphemeralSigners[] = []; - const treasuryId = getRandomTreasuryId(); + const treasuryId = this.treasuryId ?? getRandomTreasuryId(); for (const priceUpdateData of priceUpdateDataArray) { const accumulatorUpdateData = parseAccumulatorUpdateData( diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts index 18164cb7b9..47b508c3bb 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts @@ -42,10 +42,11 @@ export const VAA_START = 46; * * The first one writes the first `VAA_SPLIT_INDEX` bytes and the second one writes the rest. * - * This number was chosen as the biggest number such that one can still call `createInstruction`, `initEncodedVaa` and `writeEncodedVaa` in a single Solana transaction. + * This number was chosen as the biggest number such that one can still call `createInstruction`, + * `initEncodedVaa` and `writeEncodedVaa` in a single Solana transaction, while using an address lookup table. * This way, the packing of the instructions to post an encoded vaa is more efficient. */ -export const VAA_SPLIT_INDEX = 755; +export const VAA_SPLIT_INDEX = 721; /** * Trim the number of signatures of a VAA.