Skip to content

Commit 4bd2ada

Browse files
feat(price_pusher/solana): use an Address Lookup Table to reduce number 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
1 parent 27bba80 commit 4bd2ada

File tree

8 files changed

+130
-45
lines changed

8 files changed

+130
-45
lines changed

apps/price_pusher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/price-pusher",
3-
"version": "9.0.0",
3+
"version": "9.0.1",
44
"description": "Pyth Price Pusher",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",

apps/price_pusher/src/solana/command.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ export default {
8181
type: "number",
8282
default: 6,
8383
} as Options,
84+
"address-lookup-table-account": {
85+
description: "The pubkey of the ALT to use when updating price feeds",
86+
type: "string",
87+
optional: true,
88+
} as Options,
89+
"treasury-id": {
90+
description:
91+
"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.",
92+
type: "number",
93+
optional: true,
94+
} as Options,
8495
...options.priceConfigFile,
8596
...options.priceServiceEndpoint,
8697
...options.pythContractAddress,
@@ -107,6 +118,8 @@ export default {
107118
maxJitoTipLamports,
108119
jitoBundleSize,
109120
updatesPerJitoBundle,
121+
addressLookupTableAccount,
122+
treasuryId,
110123
logLevel,
111124
controllerLogLevel,
112125
} = argv;
@@ -145,12 +158,21 @@ export default {
145158
)
146159
);
147160

161+
const connection = new Connection(endpoint, "processed");
148162
const pythSolanaReceiver = new PythSolanaReceiver({
149-
connection: new Connection(endpoint, "processed"),
163+
connection,
150164
wallet,
151165
pushOracleProgramId: new PublicKey(pythContractAddress),
166+
treasuryId: treasuryId,
152167
});
153168

169+
// Fetch the account lookup table if provided
170+
const lookupTableAccount = addressLookupTableAccount
171+
? await connection
172+
.getAddressLookupTable(new PublicKey(addressLookupTableAccount))
173+
.then((result) => result.value ?? undefined)
174+
: undefined;
175+
154176
let solanaPricePusher;
155177
if (jitoTipLamports) {
156178
const jitoKeypair = Keypair.fromSecretKey(
@@ -168,7 +190,8 @@ export default {
168190
maxJitoTipLamports,
169191
jitoClient,
170192
jitoBundleSize,
171-
updatesPerJitoBundle
193+
updatesPerJitoBundle,
194+
lookupTableAccount
172195
);
173196

174197
onBundleResult(jitoClient, logger.child({ module: "JitoClient" }));
@@ -178,7 +201,8 @@ export default {
178201
hermesClient,
179202
logger.child({ module: "SolanaPricePusher" }),
180203
shardId,
181-
computeUnitPriceMicroLamports
204+
computeUnitPriceMicroLamports,
205+
lookupTableAccount
182206
);
183207
}
184208

apps/price_pusher/src/solana/solana.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { SearcherClient } from "jito-ts/dist/sdk/block-engine/searcher";
1515
import { sliceAccumulatorUpdateData } from "@pythnetwork/price-service-sdk";
1616
import { Logger } from "pino";
17-
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
17+
import { AddressLookupTableAccount, LAMPORTS_PER_SOL } from "@solana/web3.js";
1818

1919
const HEALTH_CHECK_TIMEOUT_SECONDS = 60;
2020

@@ -97,7 +97,8 @@ export class SolanaPricePusher implements IPricePusher {
9797
private hermesClient: HermesClient,
9898
private logger: Logger,
9999
private shardId: number,
100-
private computeUnitPriceMicroLamports: number
100+
private computeUnitPriceMicroLamports: number,
101+
private addressLookupTableAccount?: AddressLookupTableAccount
101102
) {}
102103

103104
async updatePriceFeed(priceIds: string[]): Promise<void> {
@@ -126,9 +127,12 @@ export class SolanaPricePusher implements IPricePusher {
126127
return;
127128
}
128129

129-
const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder({
130-
closeUpdateAccounts: true,
131-
});
130+
const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder(
131+
{
132+
closeUpdateAccounts: true,
133+
},
134+
this.addressLookupTableAccount
135+
);
132136
await transactionBuilder.addUpdatePriceFeed(
133137
priceFeedUpdateData,
134138
this.shardId
@@ -164,7 +168,8 @@ export class SolanaPricePusherJito implements IPricePusher {
164168
private maxJitoTipLamports: number,
165169
private searcherClient: SearcherClient,
166170
private jitoBundleSize: number,
167-
private updatesPerJitoBundle: number
171+
private updatesPerJitoBundle: number,
172+
private addressLookupTableAccount?: AddressLookupTableAccount
168173
) {}
169174

170175
async getRecentJitoTipLamports(): Promise<number | undefined> {
@@ -215,9 +220,12 @@ export class SolanaPricePusherJito implements IPricePusher {
215220
}
216221

217222
for (let i = 0; i < priceIds.length; i += this.updatesPerJitoBundle) {
218-
const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder({
219-
closeUpdateAccounts: true,
220-
});
223+
const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder(
224+
{
225+
closeUpdateAccounts: true,
226+
},
227+
this.addressLookupTableAccount
228+
);
221229
await transactionBuilder.addUpdatePriceFeed(
222230
priceFeedUpdateData.map((x) => {
223231
return sliceAccumulatorUpdateData(

target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,18 @@ async function main() {
2626
`Sending transactions from account: ${keypair.publicKey.toBase58()}`
2727
);
2828
const wallet = new Wallet(keypair);
29-
const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet });
29+
// Optionally use an account lookup table to reduce tx sizes.
30+
const addressLookupTableAccount = new PublicKey(
31+
"5DNCErWQFBdvCxWQXaC1mrEFsvL3ftrzZ2gVZWNybaSX"
32+
);
33+
// Use a stable treasury ID of 0, since its address is indexed in the address lookup table.
34+
// This is a tx size optimization and is optional. If not provided, a random treasury account will be used.
35+
const treasuryId = 1;
36+
const pythSolanaReceiver = new PythSolanaReceiver({
37+
connection,
38+
wallet,
39+
treasuryId,
40+
});
3041

3142
// Get the price update from hermes
3243
const priceUpdateData = await getPriceUpdateData();
@@ -35,9 +46,15 @@ async function main() {
3546
// If closeUpdateAccounts = true, the builder will automatically generate instructions to close the ephemeral price update accounts
3647
// at the end of the transaction. Closing the accounts will reclaim their rent.
3748
// The example is using closeUpdateAccounts = false so you can easily look up the price update account in an explorer.
38-
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
39-
closeUpdateAccounts: false,
40-
});
49+
const lookupTableAccount =
50+
(await connection.getAddressLookupTable(addressLookupTableAccount)).value ??
51+
undefined;
52+
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder(
53+
{
54+
closeUpdateAccounts: false,
55+
},
56+
lookupTableAccount
57+
);
4158
// Post the price updates to ephemeral accounts, one per price feed.
4259
await transactionBuilder.addPostPriceUpdates(priceUpdateData);
4360
console.log(

target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const SOL_PRICE_FEED_ID =
1111
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
1212
const ETH_PRICE_FEED_ID =
1313
"0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";
14+
const PRICE_FEED_IDS = [SOL_PRICE_FEED_ID, ETH_PRICE_FEED_ID];
1415

1516
let keypairFile = "";
1617
if (process.env["SOLANA_KEYPAIR"]) {
@@ -26,24 +27,45 @@ async function main() {
2627
`Sending transactions from account: ${keypair.publicKey.toBase58()}`
2728
);
2829
const wallet = new Wallet(keypair);
29-
const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet });
30+
31+
// Optionally use an account lookup table to reduce tx sizes.
32+
const addressLookupTableAccount = new PublicKey(
33+
"5DNCErWQFBdvCxWQXaC1mrEFsvL3ftrzZ2gVZWNybaSX"
34+
);
35+
// Use a stable treasury ID of 0, since its address is indexed in the address lookup table.
36+
// This is a tx size optimization and is optional. If not provided, a random treasury account will be used.
37+
const treasuryId = 1;
38+
const pythSolanaReceiver = new PythSolanaReceiver({
39+
connection,
40+
wallet,
41+
treasuryId,
42+
});
3043

3144
// Get the price update from hermes
32-
const priceUpdateData = await getPriceUpdateData();
45+
const priceUpdateData = await getPriceUpdateData(PRICE_FEED_IDS);
3346
console.log(`Posting price update: ${priceUpdateData}`);
3447

3548
// The shard indicates which set of price feed accounts you wish to update.
3649
const shardId = 1;
50+
const lookupTableAccount =
51+
(await connection.getAddressLookupTable(addressLookupTableAccount)).value ??
52+
undefined;
53+
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder(
54+
{},
55+
lookupTableAccount
56+
);
3757

38-
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
3958
// Update the price feed accounts for the feed ids in priceUpdateData (in this example, SOL and ETH) and shard id.
4059
await transactionBuilder.addUpdatePriceFeed(priceUpdateData, shardId);
41-
console.log(
42-
"The SOL/USD price update will get posted to:",
43-
pythSolanaReceiver
44-
.getPriceFeedAccountAddress(shardId, SOL_PRICE_FEED_ID)
45-
.toBase58()
46-
);
60+
// Print all price feed accounts that will be updated
61+
for (const priceFeedId of PRICE_FEED_IDS) {
62+
console.log(
63+
`The ${priceFeedId} price update will get posted to:`,
64+
pythSolanaReceiver
65+
.getPriceFeedAccountAddress(shardId, priceFeedId)
66+
.toBase58()
67+
);
68+
}
4769

4870
await transactionBuilder.addPriceConsumerInstructions(
4971
async (
@@ -69,16 +91,12 @@ async function main() {
6991
}
7092

7193
// Fetch price update data from Hermes
72-
async function getPriceUpdateData() {
73-
const priceServiceConnection = new HermesClient(
74-
"https://hermes.pyth.network/",
75-
{}
76-
);
94+
async function getPriceUpdateData(price_feed_ids: string[]) {
95+
const hermesClient = new HermesClient("https://hermes.pyth.network/", {});
7796

78-
const response = await priceServiceConnection.getLatestPriceUpdates(
79-
[SOL_PRICE_FEED_ID, ETH_PRICE_FEED_ID],
80-
{ encoding: "base64" }
81-
);
97+
const response = await hermesClient.getLatestPriceUpdates(price_feed_ids, {
98+
encoding: "base64",
99+
});
82100

83101
return response.binary.data;
84102
}

target_chains/solana/sdk/js/pyth_solana_receiver/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/pyth-solana-receiver",
3-
"version": "0.9.1",
3+
"version": "0.10.0",
44
"description": "Pyth solana receiver SDK",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",

target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ export type PythTransactionBuilderConfig = {
6666
closeUpdateAccounts?: boolean;
6767
};
6868

69+
/**
70+
* A stable treasury ID. This ID's corresponding treasury address
71+
* can be cached in an account lookup table in order to reduce the overall txn size.
72+
*/
73+
export const DEFAULT_TREASURY_ID = 0;
74+
6975
/**
7076
* A builder class to build transactions that:
7177
* - Post price updates (fully or partially verified) or update price feed accounts
@@ -430,20 +436,29 @@ export class PythSolanaReceiver {
430436
readonly receiver: Program<PythSolanaReceiverProgram>;
431437
readonly wormhole: Program<WormholeCoreBridgeSolana>;
432438
readonly pushOracle: Program<PythPushOracle>;
433-
439+
readonly treasuryId?: number;
434440
constructor({
435441
connection,
436442
wallet,
437443
wormholeProgramId = DEFAULT_WORMHOLE_PROGRAM_ID,
438444
receiverProgramId = DEFAULT_RECEIVER_PROGRAM_ID,
439445
pushOracleProgramId = DEFAULT_PUSH_ORACLE_PROGRAM_ID,
446+
treasuryId = undefined,
440447
}: {
441448
connection: Connection;
442449
wallet: Wallet;
443450
wormholeProgramId?: PublicKey;
444451
receiverProgramId?: PublicKey;
445452
pushOracleProgramId?: PublicKey;
453+
// Optionally provide a treasuryId to always use a specific treasury account.
454+
// This can be useful when using an ALT to reduce tx size.
455+
// If not provided, treasury accounts will be randomly selected.
456+
treasuryId?: number;
446457
}) {
458+
if (treasuryId !== undefined && (treasuryId < 0 || treasuryId > 255)) {
459+
throw new Error("treasuryId must be between 0 and 255");
460+
}
461+
447462
this.connection = connection;
448463
this.wallet = wallet;
449464
this.provider = new AnchorProvider(this.connection, this.wallet, {
@@ -464,15 +479,17 @@ export class PythSolanaReceiver {
464479
pushOracleProgramId,
465480
this.provider
466481
);
482+
this.treasuryId = treasuryId;
467483
}
468484

469485
/**
470486
* Get a new transaction builder to build transactions that interact with the Pyth Solana Receiver program and consume price updates
471487
*/
472488
newTransactionBuilder(
473-
config: PythTransactionBuilderConfig
489+
config: PythTransactionBuilderConfig,
490+
addressLookupAccount?: AddressLookupTableAccount
474491
): PythTransactionBuilder {
475-
return new PythTransactionBuilder(this, config);
492+
return new PythTransactionBuilder(this, config, addressLookupAccount);
476493
}
477494

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

500-
const treasuryId = getRandomTreasuryId();
517+
const treasuryId = this.treasuryId ?? getRandomTreasuryId();
501518

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

568-
const treasuryId = getRandomTreasuryId();
585+
const treasuryId = this.treasuryId ?? getRandomTreasuryId();
569586

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

639-
const treasuryId = getRandomTreasuryId();
656+
const treasuryId = this.treasuryId ?? getRandomTreasuryId();
640657

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

733-
const treasuryId = getRandomTreasuryId();
750+
const treasuryId = this.treasuryId ?? getRandomTreasuryId();
734751

735752
for (const priceUpdateData of priceUpdateDataArray) {
736753
const accumulatorUpdateData = parseAccumulatorUpdateData(

target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ export const VAA_START = 46;
4242
*
4343
* The first one writes the first `VAA_SPLIT_INDEX` bytes and the second one writes the rest.
4444
*
45-
* This number was chosen as the biggest number such that one can still call `createInstruction`, `initEncodedVaa` and `writeEncodedVaa` in a single Solana transaction.
45+
* This number was chosen as the biggest number such that one can still call `createInstruction`,
46+
* `initEncodedVaa` and `writeEncodedVaa` in a single Solana transaction, while using an address lookup table.
4647
* This way, the packing of the instructions to post an encoded vaa is more efficient.
4748
*/
48-
export const VAA_SPLIT_INDEX = 755;
49+
export const VAA_SPLIT_INDEX = 721;
4950

5051
/**
5152
* Trim the number of signatures of a VAA.

0 commit comments

Comments
 (0)