Skip to content

feat(price_pusher/solana): use an Address Lookup Table to reduce number of txs #2396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading