Skip to content

Commit

Permalink
feat(price_pusher/solana): use an Address Lookup Table to reduce numb…
Browse files Browse the repository at this point in the history
…er of txs (#2396)

* feat: add ALT to pusher to reduce number of txs

* bump ver

* fix: add configurable treasuryId, set tighter vaa split, improve examples

* feat: bump pusher

* feat(pusher): expose treasury-id param
  • Loading branch information
tejasbadadare authored Feb 20, 2025
1 parent 27bba80 commit 4bd2ada
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 45 deletions.
2 changes: 1 addition & 1 deletion apps/price_pusher/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
30 changes: 27 additions & 3 deletions apps/price_pusher/src/solana/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -107,6 +118,8 @@ export default {
maxJitoTipLamports,
jitoBundleSize,
updatesPerJitoBundle,
addressLookupTableAccount,
treasuryId,
logLevel,
controllerLogLevel,
} = argv;
Expand Down Expand Up @@ -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(
Expand All @@ -168,7 +190,8 @@ export default {
maxJitoTipLamports,
jitoClient,
jitoBundleSize,
updatesPerJitoBundle
updatesPerJitoBundle,
lookupTableAccount
);

onBundleResult(jitoClient, logger.child({ module: "JitoClient" }));
Expand All @@ -178,7 +201,8 @@ export default {
hermesClient,
logger.child({ module: "SolanaPricePusher" }),
shardId,
computeUnitPriceMicroLamports
computeUnitPriceMicroLamports,
lookupTableAccount
);
}

Expand Down
26 changes: 17 additions & 9 deletions apps/price_pusher/src/solana/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<void> {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<number | undefined> {
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]) {
Expand All @@ -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 (
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -430,20 +436,29 @@ export class PythSolanaReceiver {
readonly receiver: Program<PythSolanaReceiverProgram>;
readonly wormhole: Program<WormholeCoreBridgeSolana>;
readonly pushOracle: Program<PythPushOracle>;

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, {
Expand All @@ -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);
}

/**
Expand All @@ -497,7 +514,7 @@ export class PythSolanaReceiver {
const priceFeedIdToPriceUpdateAccount: Record<string, PublicKey> = {};
const closeInstructions: InstructionWithEphemeralSigners[] = [];

const treasuryId = getRandomTreasuryId();
const treasuryId = this.treasuryId ?? getRandomTreasuryId();

for (const priceUpdateData of priceUpdateDataArray) {
const accumulatorUpdateData = parseAccumulatorUpdateData(
Expand Down Expand Up @@ -565,7 +582,7 @@ export class PythSolanaReceiver {
const priceFeedIdToPriceUpdateAccount: Record<string, PublicKey> = {};
const closeInstructions: InstructionWithEphemeralSigners[] = [];

const treasuryId = getRandomTreasuryId();
const treasuryId = this.treasuryId ?? getRandomTreasuryId();

for (const priceUpdateData of priceUpdateDataArray) {
const accumulatorUpdateData = parseAccumulatorUpdateData(
Expand Down Expand Up @@ -636,7 +653,7 @@ export class PythSolanaReceiver {
const priceFeedIdToTwapUpdateAccount: Record<string, PublicKey> = {};
const closeInstructions: InstructionWithEphemeralSigners[] = [];

const treasuryId = getRandomTreasuryId();
const treasuryId = this.treasuryId ?? getRandomTreasuryId();

if (twapUpdateDataArray.length !== 2) {
throw new Error(
Expand Down Expand Up @@ -730,7 +747,7 @@ export class PythSolanaReceiver {
const priceFeedIdToPriceUpdateAccount: Record<string, PublicKey> = {};
const closeInstructions: InstructionWithEphemeralSigners[] = [];

const treasuryId = getRandomTreasuryId();
const treasuryId = this.treasuryId ?? getRandomTreasuryId();

for (const priceUpdateData of priceUpdateDataArray) {
const accumulatorUpdateData = parseAccumulatorUpdateData(
Expand Down
5 changes: 3 additions & 2 deletions target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 4bd2ada

Please sign in to comment.