@pythnetwork/pyth-solana-receiver 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +0 -44
  2. package/dist/cjs/PythSolanaReceiver.cjs +442 -0
  3. package/{lib → dist/cjs}/PythSolanaReceiver.d.ts +10 -86
  4. package/dist/cjs/address.cjs +63 -0
  5. package/{lib → dist/cjs}/address.d.ts +0 -1
  6. package/dist/cjs/compute_budget.cjs +42 -0
  7. package/{lib → dist/cjs}/compute_budget.d.ts +0 -5
  8. package/dist/cjs/idl/pyth_push_oracle.cjs +132 -0
  9. package/{lib → dist/cjs}/idl/pyth_push_oracle.d.ts +0 -1
  10. package/dist/cjs/idl/pyth_solana_receiver.cjs +858 -0
  11. package/{lib → dist/cjs}/idl/pyth_solana_receiver.d.ts +0 -1
  12. package/dist/cjs/idl/wormhole_core_bridge_solana.cjs +1751 -0
  13. package/{lib → dist/cjs}/idl/wormhole_core_bridge_solana.d.ts +0 -1
  14. package/dist/cjs/index.cjs +44 -0
  15. package/dist/cjs/index.d.ts +5 -0
  16. package/dist/cjs/package.json +1 -0
  17. package/dist/cjs/vaa.cjs +192 -0
  18. package/{lib → dist/cjs}/vaa.d.ts +4 -30
  19. package/dist/esm/PythSolanaReceiver.d.ts +318 -0
  20. package/dist/esm/PythSolanaReceiver.mjs +462 -0
  21. package/dist/esm/address.d.ts +29 -0
  22. package/dist/esm/address.mjs +45 -0
  23. package/dist/esm/compute_budget.d.ts +28 -0
  24. package/dist/esm/compute_budget.mjs +21 -0
  25. package/dist/esm/idl/pyth_push_oracle.d.ts +117 -0
  26. package/{lib/idl/pyth_push_oracle.js → dist/esm/idl/pyth_push_oracle.mjs} +41 -38
  27. package/dist/esm/idl/pyth_solana_receiver.d.ts +840 -0
  28. package/{lib/idl/pyth_solana_receiver.js → dist/esm/idl/pyth_solana_receiver.mjs} +237 -231
  29. package/dist/esm/idl/wormhole_core_bridge_solana.d.ts +1606 -0
  30. package/{lib/idl/wormhole_core_bridge_solana.js → dist/esm/idl/wormhole_core_bridge_solana.mjs} +598 -465
  31. package/dist/esm/index.d.ts +5 -0
  32. package/dist/esm/index.mjs +5 -0
  33. package/dist/esm/package.json +1 -0
  34. package/dist/esm/vaa.d.ts +75 -0
  35. package/dist/esm/vaa.mjs +197 -0
  36. package/package.json +101 -13
  37. package/lib/PythSolanaReceiver.d.ts.map +0 -1
  38. package/lib/PythSolanaReceiver.js +0 -633
  39. package/lib/address.d.ts.map +0 -1
  40. package/lib/address.js +0 -46
  41. package/lib/compute_budget.d.ts.map +0 -1
  42. package/lib/compute_budget.js +0 -35
  43. package/lib/idl/pyth_push_oracle.d.ts.map +0 -1
  44. package/lib/idl/pyth_solana_receiver.d.ts.map +0 -1
  45. package/lib/idl/pyth_solana_receiver.json +0 -839
  46. package/lib/idl/wormhole_core_bridge_solana.d.ts.map +0 -1
  47. package/lib/index.d.ts +0 -6
  48. package/lib/index.d.ts.map +0 -1
  49. package/lib/index.js +0 -17
  50. package/lib/vaa.d.ts.map +0 -1
  51. package/lib/vaa.js +0 -270
@@ -0,0 +1,462 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable tsdoc/syntax */ // eslint-disable-next-line unicorn/prefer-node-protocol
2
+ import { Buffer as IsomorphicBuffer } from "buffer";
3
+ import { AnchorProvider, Program } from "@coral-xyz/anchor";
4
+ import { parseAccumulatorUpdateData, parsePriceFeedMessage } from "@pythnetwork/price-service-sdk";
5
+ import { TransactionBuilder } from "@pythnetwork/solana-utils";
6
+ import { PublicKey, Keypair } from "@solana/web3.js";
7
+ import { DEFAULT_PUSH_ORACLE_PROGRAM_ID, DEFAULT_RECEIVER_PROGRAM_ID, DEFAULT_WORMHOLE_PROGRAM_ID, getConfigPda, getGuardianSetPda, getRandomTreasuryId, getTreasuryPda } from "./address.mjs";
8
+ import { POST_UPDATE_ATOMIC_COMPUTE_BUDGET, POST_UPDATE_COMPUTE_BUDGET, UPDATE_PRICE_FEED_COMPUTE_BUDGET } from "./compute_budget.mjs";
9
+ import { IDL as PythPushOracleIdl } from "./idl/pyth_push_oracle.mjs";
10
+ import { IDL as Idl } from "./idl/pyth_solana_receiver.mjs";
11
+ import { IDL as WormholeCoreBridgeSolanaIdl } from "./idl/wormhole_core_bridge_solana.mjs";
12
+ import { buildCloseEncodedVaaInstruction, buildPostEncodedVaaInstructions, findEncodedVaaAccountsByWriteAuthority, getGuardianSetIndex, trimSignatures } from "./vaa.mjs";
13
+ /**
14
+ * A stable treasury ID. This ID's corresponding treasury address
15
+ * can be cached in an account lookup table in order to reduce the overall txn size.
16
+ */ export const DEFAULT_TREASURY_ID = 0;
17
+ /**
18
+ * A builder class to build transactions that:
19
+ * - Post price updates (fully or partially verified) or update price feed accounts
20
+ * - Consume price updates in a consumer program
21
+ * - (Optionally) Close price update and encoded vaa accounts to recover the rent (`closeUpdateAccounts` in `PythTransactionBuilderConfig`)
22
+ *
23
+ * This class provides methods for working with both price update accounts and price feed accounts.
24
+ * Price update accounts are ephemeral accounts containing a single price update, whereas price feed accounts are long-lived
25
+ * accounts that always hold price data for a specific feed id. Price feed accounts can be updated to advance the current price.
26
+ * Applications should choose which type of account to work with based on their needs. In general, applications that
27
+ * want the price at a specific time (e.g., to settle a trade) should use price update accounts, while applications that want
28
+ * any recent price should use price feed accounts.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ *
33
+ * // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable
34
+ * const priceUpdateData = await priceServiceConnection.getLatestVaas([
35
+ * SOL_PRICE_FEED_ID,
36
+ * ETH_PRICE_FEED_ID,
37
+ * ]);
38
+ *
39
+ * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
40
+ * await transactionBuilder.addPostPriceUpdates(priceUpdateData);
41
+ * console.log("The SOL/USD price update will get posted to:", transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58())
42
+ * await transactionBuilder.addPriceConsumerInstructions(...)
43
+ *
44
+ * await pythSolanaReceiver.provider.sendAll(await transactionBuilder.buildVersionedTransactions({computeUnitPriceMicroLamports:100000, tightComputeBudget: true}))
45
+ * ```
46
+ */ export class PythTransactionBuilder extends TransactionBuilder {
47
+ pythSolanaReceiver;
48
+ closeInstructions;
49
+ priceFeedIdToPriceUpdateAccount;
50
+ closeUpdateAccounts;
51
+ constructor(pythSolanaReceiver, config, addressLookupTable){
52
+ super(pythSolanaReceiver.wallet.publicKey, pythSolanaReceiver.connection, addressLookupTable);
53
+ this.pythSolanaReceiver = pythSolanaReceiver;
54
+ this.closeInstructions = [];
55
+ this.priceFeedIdToPriceUpdateAccount = {};
56
+ this.closeUpdateAccounts = config.closeUpdateAccounts ?? true;
57
+ }
58
+ /**
59
+ * Add instructions to post price updates to the builder.
60
+ * Use this function to post fully verified price updates from the present or from the past for your program to consume.
61
+ *
62
+ * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable
67
+ * const priceUpdateData = await priceServiceConnection.getLatestVaas([
68
+ * SOL_PRICE_FEED_ID,
69
+ * ETH_PRICE_FEED_ID,
70
+ * ]);
71
+ *
72
+ * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
73
+ * await transactionBuilder.addPostPriceUpdates(priceUpdateData);
74
+ * console.log("The SOL/USD price update will get posted to:", transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58())
75
+ * await transactionBuilder.addPriceConsumerInstructions(...)
76
+ * ```
77
+ */ async addPostPriceUpdates(priceUpdateDataArray) {
78
+ const { postInstructions, priceFeedIdToPriceUpdateAccount, closeInstructions } = await this.pythSolanaReceiver.buildPostPriceUpdateInstructions(priceUpdateDataArray);
79
+ this.closeInstructions.push(...closeInstructions);
80
+ Object.assign(this.priceFeedIdToPriceUpdateAccount, priceFeedIdToPriceUpdateAccount);
81
+ this.addInstructions(postInstructions);
82
+ }
83
+ /**
84
+ * Add instructions to post partially verified price updates to the builder.
85
+ * Use this function to post partially verified price updates from the present or from the past for your program to consume.
86
+ *
87
+ * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
88
+ *
89
+ * Partially verified price updates are price updates where not all the guardian signatures have been verified. By default this methods checks `DEFAULT_REDUCED_GUARDIAN_SET_SIZE` signatures when posting the VAA.
90
+ * If you are a on-chain program developer, make sure you understand the risks of consuming partially verified price updates here: {@link https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs}.
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable
95
+ * const priceUpdateData = await priceServiceConnection.getLatestVaas([
96
+ * SOL_PRICE_FEED_ID,
97
+ * ETH_PRICE_FEED_ID,
98
+ * ]);
99
+ *
100
+ * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
101
+ * await transactionBuilder.addPostPartiallyVerifiedPriceUpdates(priceUpdateData);
102
+ * console.log("The SOL/USD price update will get posted to:", transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58())
103
+ * await transactionBuilder.addPriceConsumerInstructions(...)
104
+ * ...
105
+ * ```
106
+ */ async addPostPartiallyVerifiedPriceUpdates(priceUpdateDataArray) {
107
+ const { postInstructions, priceFeedIdToPriceUpdateAccount, closeInstructions } = await this.pythSolanaReceiver.buildPostPriceUpdateAtomicInstructions(priceUpdateDataArray);
108
+ this.closeInstructions.push(...closeInstructions);
109
+ Object.assign(this.priceFeedIdToPriceUpdateAccount, priceFeedIdToPriceUpdateAccount);
110
+ this.addInstructions(postInstructions);
111
+ }
112
+ /**
113
+ * Add instructions to update price feed accounts to the builder.
114
+ * Price feed accounts are fixed accounts per price feed id that can only be updated with a more recent price.
115
+ *
116
+ * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
117
+ * @param shardId the shard ID of the set of price feed accounts. This shard ID allows for multiple price feed accounts for the same price feed id to exist.
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable
122
+ * const priceUpdateData = await priceServiceConnection.getLatestVaas([
123
+ * SOL_PRICE_FEED_ID,
124
+ * ETH_PRICE_FEED_ID,
125
+ * ]);
126
+ *
127
+ * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
128
+ * await transactionBuilder.addUpdatePriceFeed(priceUpdateData);
129
+ * await transactionBuilder.addPriceConsumerInstructions(...)
130
+ * ...
131
+ * ```
132
+ */ async addUpdatePriceFeed(priceUpdateDataArray, shardId) {
133
+ const { postInstructions, priceFeedIdToPriceUpdateAccount, closeInstructions } = await this.pythSolanaReceiver.buildUpdatePriceFeedInstructions(priceUpdateDataArray, shardId);
134
+ this.closeInstructions.push(...closeInstructions);
135
+ Object.assign(this.priceFeedIdToPriceUpdateAccount, priceFeedIdToPriceUpdateAccount);
136
+ this.addInstructions(postInstructions);
137
+ }
138
+ /**
139
+ * Add instructions that consume price updates to the builder.
140
+ *
141
+ * @param getInstructions a function that given a mapping of price feed IDs to price update accounts, generates a series of instructions. Price updates get posted to ephemeral accounts and this function allows the user to indicate which accounts in their instruction need to be "replaced" with each price update account.
142
+ * If multiple price updates for the same price feed ID are posted with the same builder, the account corresponding to the last update to get posted will be used.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * ...
147
+ * await transactionBuilder.addPostPriceUpdates(priceUpdateData);
148
+ * await transactionBuilder.addPriceConsumerInstructions(
149
+ * async (
150
+ * getPriceUpdateAccount: ( priceFeedId: string) => PublicKey
151
+ * ): Promise<InstructionWithEphemeralSigners[]> => {
152
+ * return [
153
+ * {
154
+ * instruction: await myFirstPythApp.methods
155
+ * .consume()
156
+ * .accounts({
157
+ * solPriceUpdate: getPriceUpdateAccount(SOL_PRICE_FEED_ID),
158
+ * ethPriceUpdate: getPriceUpdateAccount(ETH_PRICE_FEED_ID),
159
+ * })
160
+ * .instruction(),
161
+ * signers: [],
162
+ * },
163
+ * ];
164
+ * }
165
+ * );
166
+ * ```
167
+ */ async addPriceConsumerInstructions(getInstructions) {
168
+ this.addInstructions(await getInstructions(this.getPriceUpdateAccount.bind(this)));
169
+ }
170
+ /** Add instructions to close encoded VAA accounts from previous actions.
171
+ * If you have previously used the PythTransactionBuilder with closeUpdateAccounts set to false or if you posted encoded VAAs but the transaction to close them did not land on-chain, your wallet might own many encoded VAA accounts.
172
+ * The rent cost for these accounts is 0.008 SOL per encoded VAA account. You can recover this rent calling this function when building a set of transactions.
173
+ */ async addClosePreviousEncodedVaasInstructions(maxInstructions = 40) {
174
+ this.addInstructions(await this.pythSolanaReceiver.buildClosePreviousEncodedVaasInstructions(maxInstructions));
175
+ }
176
+ /**
177
+ * Returns all the added instructions batched into versioned transactions, plus for each transaction the ephemeral signers that need to sign it
178
+ */ async buildVersionedTransactions(args) {
179
+ if (this.closeUpdateAccounts) {
180
+ this.addInstructions(this.closeInstructions);
181
+ }
182
+ return super.buildVersionedTransactions(args);
183
+ }
184
+ /**
185
+ * Returns all the added instructions batched into transactions, plus for each transaction the ephemeral signers that need to sign it
186
+ */ buildLegacyTransactions(args) {
187
+ if (this.closeUpdateAccounts) {
188
+ this.addInstructions(this.closeInstructions);
189
+ }
190
+ return super.buildLegacyTransactions(args);
191
+ }
192
+ /**
193
+ * This method is used to retrieve the address of the price update account where the price update for a given price feed ID will be posted.
194
+ * If multiple price updates for the same price feed ID will be posted with the same builder, the address of the account corresponding to the last update to get posted will be returned.
195
+ * */ getPriceUpdateAccount(priceFeedId) {
196
+ const priceUpdateAccount = this.priceFeedIdToPriceUpdateAccount[priceFeedId];
197
+ if (!priceUpdateAccount) {
198
+ throw new Error(`No price update account found for the price feed ID ${priceFeedId}. Make sure to call addPostPriceUpdates or addPostPartiallyVerifiedPriceUpdates before calling this function.`);
199
+ }
200
+ return priceUpdateAccount;
201
+ }
202
+ }
203
+ /**
204
+ * A class to interact with the Pyth Solana Receiver program.
205
+ *
206
+ * This class provides helpful methods to build instructions to interact with the Pyth Solana Receiver program:
207
+ * - Post price updates (fully or partially verified)
208
+ * - Close price update and encoded vaa accounts to recover rent
209
+ */ export class PythSolanaReceiver {
210
+ connection;
211
+ wallet;
212
+ provider;
213
+ receiver;
214
+ wormhole;
215
+ pushOracle;
216
+ treasuryId;
217
+ constructor({ connection, wallet, wormholeProgramId = DEFAULT_WORMHOLE_PROGRAM_ID, receiverProgramId = DEFAULT_RECEIVER_PROGRAM_ID, pushOracleProgramId = DEFAULT_PUSH_ORACLE_PROGRAM_ID, treasuryId }){
218
+ if (treasuryId !== undefined && (treasuryId < 0 || treasuryId > 255)) {
219
+ throw new Error("treasuryId must be between 0 and 255");
220
+ }
221
+ this.connection = connection;
222
+ this.wallet = wallet;
223
+ this.provider = new AnchorProvider(this.connection, this.wallet, {
224
+ commitment: connection.commitment
225
+ });
226
+ this.receiver = new Program(Idl, receiverProgramId, this.provider);
227
+ this.wormhole = new Program(WormholeCoreBridgeSolanaIdl, wormholeProgramId, this.provider);
228
+ this.pushOracle = new Program(PythPushOracleIdl, pushOracleProgramId, this.provider);
229
+ this.treasuryId = treasuryId;
230
+ }
231
+ /**
232
+ * Get a new transaction builder to build transactions that interact with the Pyth Solana Receiver program and consume price updates
233
+ */ newTransactionBuilder(config, addressLookupAccount) {
234
+ return new PythTransactionBuilder(this, config, addressLookupAccount);
235
+ }
236
+ /**
237
+ * Build a series of helper instructions that post price updates to the Pyth Solana Receiver program and another series to close the price update accounts.
238
+ *
239
+ * This function uses partially verified price updates. Partially verified price updates are price updates where not all the guardian signatures have been verified. By default this methods checks `DEFAULT_REDUCED_GUARDIAN_SET_SIZE` signatures when posting the VAA.
240
+ * If you are a on-chain program developer, make sure you understand the risks of consuming partially verified price updates here: {@link https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs}.
241
+ *
242
+ * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
243
+ * @returns `postInstructions`: the instructions to post the price updates, these should be called before consuming the price updates
244
+ * @returns `priceFeedIdToPriceUpdateAccount`: this is a map of price feed IDs to Solana address. Given a price feed ID, you can use this map to find the account where `postInstructions` will post the price update.
245
+ * @returns `closeInstructions`: the instructions to close the price update accounts, these should be called after consuming the price updates
246
+ */ async buildPostPriceUpdateAtomicInstructions(priceUpdateDataArray) {
247
+ const postInstructions = [];
248
+ const priceFeedIdToPriceUpdateAccount = {};
249
+ const closeInstructions = [];
250
+ const treasuryId = this.treasuryId ?? getRandomTreasuryId();
251
+ for (const priceUpdateData of priceUpdateDataArray){
252
+ const accumulatorUpdateData = parseAccumulatorUpdateData(IsomorphicBuffer.from(priceUpdateData, "base64"));
253
+ const guardianSetIndex = getGuardianSetIndex(accumulatorUpdateData.vaa);
254
+ const trimmedVaa = trimSignatures(accumulatorUpdateData.vaa);
255
+ for (const update of accumulatorUpdateData.updates){
256
+ const priceUpdateKeypair = new Keypair();
257
+ postInstructions.push({
258
+ instruction: await this.receiver.methods.postUpdateAtomic({
259
+ vaa: trimmedVaa,
260
+ merklePriceUpdate: update,
261
+ treasuryId
262
+ }).accounts({
263
+ priceUpdateAccount: priceUpdateKeypair.publicKey,
264
+ treasury: getTreasuryPda(treasuryId, this.receiver.programId),
265
+ config: getConfigPda(this.receiver.programId),
266
+ guardianSet: getGuardianSetPda(guardianSetIndex, this.wormhole.programId)
267
+ }).instruction(),
268
+ signers: [
269
+ priceUpdateKeypair
270
+ ],
271
+ computeUnits: POST_UPDATE_ATOMIC_COMPUTE_BUDGET
272
+ });
273
+ priceFeedIdToPriceUpdateAccount["0x" + parsePriceFeedMessage(update.message).feedId.toString("hex")] = priceUpdateKeypair.publicKey;
274
+ closeInstructions.push(await this.buildClosePriceUpdateInstruction(priceUpdateKeypair.publicKey));
275
+ }
276
+ }
277
+ return {
278
+ postInstructions,
279
+ priceFeedIdToPriceUpdateAccount,
280
+ closeInstructions
281
+ };
282
+ }
283
+ /**
284
+ * Build a series of helper instructions that post price updates to the Pyth Solana Receiver program and another series to close the encoded vaa accounts and the price update accounts.
285
+ *
286
+ * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
287
+ * @returns `postInstructions`: the instructions to post the price updates, these should be called before consuming the price updates
288
+ * @returns `priceFeedIdToPriceUpdateAccount`: this is a map of price feed IDs to Solana address. Given a price feed ID, you can use this map to find the account where `postInstructions` will post the price update.
289
+ * @returns `closeInstructions`: the instructions to close the price update accounts, these should be called after consuming the price updates
290
+ */ async buildPostPriceUpdateInstructions(priceUpdateDataArray) {
291
+ const postInstructions = [];
292
+ const priceFeedIdToPriceUpdateAccount = {};
293
+ const closeInstructions = [];
294
+ const treasuryId = this.treasuryId ?? getRandomTreasuryId();
295
+ for (const priceUpdateData of priceUpdateDataArray){
296
+ const accumulatorUpdateData = parseAccumulatorUpdateData(IsomorphicBuffer.from(priceUpdateData, "base64"));
297
+ const { postInstructions: postEncodedVaaInstructions, encodedVaaAddress: encodedVaa, closeInstructions: postEncodedVaacloseInstructions } = await this.buildPostEncodedVaaInstructions(accumulatorUpdateData.vaa);
298
+ postInstructions.push(...postEncodedVaaInstructions);
299
+ closeInstructions.push(...postEncodedVaacloseInstructions);
300
+ for (const update of accumulatorUpdateData.updates){
301
+ const priceUpdateKeypair = new Keypair();
302
+ postInstructions.push({
303
+ instruction: await this.receiver.methods.postUpdate({
304
+ merklePriceUpdate: update,
305
+ treasuryId
306
+ }).accounts({
307
+ encodedVaa,
308
+ priceUpdateAccount: priceUpdateKeypair.publicKey,
309
+ treasury: getTreasuryPda(treasuryId, this.receiver.programId),
310
+ config: getConfigPda(this.receiver.programId)
311
+ }).instruction(),
312
+ signers: [
313
+ priceUpdateKeypair
314
+ ],
315
+ computeUnits: POST_UPDATE_COMPUTE_BUDGET
316
+ });
317
+ priceFeedIdToPriceUpdateAccount["0x" + parsePriceFeedMessage(update.message).feedId.toString("hex")] = priceUpdateKeypair.publicKey;
318
+ closeInstructions.push(await this.buildClosePriceUpdateInstruction(priceUpdateKeypair.publicKey));
319
+ }
320
+ }
321
+ return {
322
+ postInstructions,
323
+ priceFeedIdToPriceUpdateAccount,
324
+ closeInstructions
325
+ };
326
+ }
327
+ /**
328
+ * Build a series of helper instructions that update one or many price feed accounts and another series to close the encoded vaa accounts used to update the price feed accounts.
329
+ *
330
+ * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
331
+ * @param shardId the shard ID of the set of price feed accounts. This shard ID allows for multiple price feed accounts for the same price feed id to exist.
332
+ * @returns `postInstructions`: the instructions to update the price feed accounts. If the price feed accounts don't contain a recent update, these should be called before consuming the price updates.
333
+ * @returns `priceFeedIdToPriceUpdateAccount`: this is a map of price feed IDs to Solana address. Given a price feed ID, you can use this map to find the account where `postInstructions` will post the price update. Note that since price feed accounts are PDAs, the address of the account can also be found with `getPriceFeedAccountAddress`.
334
+ * @returns `closeInstructions`: the instructions to close the encoded VAA accounts that were used to update the price feed accounts.
335
+ */ async buildUpdatePriceFeedInstructions(priceUpdateDataArray, shardId) {
336
+ const postInstructions = [];
337
+ const priceFeedIdToPriceUpdateAccount = {};
338
+ const closeInstructions = [];
339
+ const treasuryId = this.treasuryId ?? getRandomTreasuryId();
340
+ for (const priceUpdateData of priceUpdateDataArray){
341
+ const accumulatorUpdateData = parseAccumulatorUpdateData(IsomorphicBuffer.from(priceUpdateData, "base64"));
342
+ const { postInstructions: postEncodedVaaInstructions, encodedVaaAddress: encodedVaa, closeInstructions: postEncodedVaacloseInstructions } = await this.buildPostEncodedVaaInstructions(accumulatorUpdateData.vaa);
343
+ postInstructions.push(...postEncodedVaaInstructions);
344
+ closeInstructions.push(...postEncodedVaacloseInstructions);
345
+ for (const update of accumulatorUpdateData.updates){
346
+ const feedId = parsePriceFeedMessage(update.message).feedId;
347
+ postInstructions.push({
348
+ instruction: await this.pushOracle.methods.updatePriceFeed({
349
+ merklePriceUpdate: update,
350
+ treasuryId
351
+ }, shardId, [
352
+ ...feedId
353
+ ]).accounts({
354
+ pythSolanaReceiver: this.receiver.programId,
355
+ encodedVaa,
356
+ priceFeedAccount: this.getPriceFeedAccountAddress(shardId, feedId),
357
+ treasury: getTreasuryPda(treasuryId, this.receiver.programId),
358
+ config: getConfigPda(this.receiver.programId)
359
+ }).instruction(),
360
+ signers: [],
361
+ computeUnits: UPDATE_PRICE_FEED_COMPUTE_BUDGET
362
+ });
363
+ priceFeedIdToPriceUpdateAccount["0x" + parsePriceFeedMessage(update.message).feedId.toString("hex")] = this.getPriceFeedAccountAddress(shardId, feedId);
364
+ }
365
+ }
366
+ return {
367
+ postInstructions,
368
+ priceFeedIdToPriceUpdateAccount,
369
+ closeInstructions
370
+ };
371
+ }
372
+ /**
373
+ * Build a series of helper instructions that post a VAA in an encoded VAA account. This function is bespoke for posting Pyth VAAs and might not work for other usecases.
374
+ *
375
+ * @param vaa a Wormhole VAA
376
+ * @returns `encodedVaaAddress`: the address of the encoded VAA account where the VAA will be posted
377
+ * @returns `postInstructions`: the instructions to post the VAA
378
+ * @returns `closeInstructions`: the instructions to close the encoded VAA account
379
+ */ async buildPostEncodedVaaInstructions(vaa) {
380
+ return buildPostEncodedVaaInstructions(this.wormhole, vaa);
381
+ }
382
+ /**
383
+ * Build an instruction to close an encoded VAA account, recovering the rent.
384
+ */ async buildCloseEncodedVaaInstruction(encodedVaa) {
385
+ return buildCloseEncodedVaaInstruction(this.wormhole, encodedVaa);
386
+ }
387
+ /**
388
+ * Build aset of instructions to close all the existing encoded VAA accounts owned by this PythSolanaReceiver's wallet
389
+ */ async buildClosePreviousEncodedVaasInstructions(maxInstructions) {
390
+ const encodedVaas = await this.findOwnedEncodedVaaAccounts();
391
+ const instructions = [];
392
+ for (const encodedVaa of encodedVaas){
393
+ instructions.push(await this.buildCloseEncodedVaaInstruction(encodedVaa));
394
+ }
395
+ return instructions.slice(0, maxInstructions);
396
+ }
397
+ /**
398
+ * Build an instruction to close a price update account, recovering the rent.
399
+ */ async buildClosePriceUpdateInstruction(priceUpdateAccount) {
400
+ const instruction = await this.receiver.methods.reclaimRent().accounts({
401
+ priceUpdateAccount
402
+ }).instruction();
403
+ return {
404
+ instruction,
405
+ signers: []
406
+ };
407
+ }
408
+ /**
409
+ * Returns a set of versioned transactions that contain the provided instructions in the same order and with efficient batching
410
+ */ async batchIntoVersionedTransactions(instructions, priorityFeeConfig, addressLookupTable) {
411
+ return TransactionBuilder.batchIntoVersionedTransactions(this.wallet.publicKey, this.connection, instructions, priorityFeeConfig, addressLookupTable);
412
+ }
413
+ /**
414
+ * Fetch the contents of a price update account
415
+ * @param priceUpdateAccount The address of the price update account
416
+ * @returns The contents of the deserialized price update account or `null` if the account doesn't exist
417
+ */ async fetchPriceUpdateAccount(priceUpdateAccount) {
418
+ return this.receiver.account.priceUpdateV2.fetchNullable(priceUpdateAccount);
419
+ }
420
+ /**
421
+ * Fetch the contents of a price feed account
422
+ * @param shardId The shard ID of the set of price feed accounts. This shard ID allows for multiple price feed accounts for the same price feed id to exist.
423
+ * @param priceFeedId The price feed ID, as either a 32-byte buffer or hexadecimal string with or without a leading "0x" prefix.
424
+ * @returns The contents of the deserialized price feed account or `null` if the account doesn't exist
425
+ */ async fetchPriceFeedAccount(shardId, priceFeedId) {
426
+ return this.receiver.account.priceUpdateV2.fetchNullable(this.getPriceFeedAccountAddress(shardId, priceFeedId));
427
+ }
428
+ /**
429
+ * Derive the address of a price feed account
430
+ * @param shardId The shard ID of the set of price feed accounts. This shard ID allows for multiple price feed accounts for the same price feed id to exist.
431
+ * @param priceFeedId The price feed ID, as either a 32-byte buffer or hexadecimal string with or without a leading "0x" prefix.
432
+ * @returns The address of the price feed account
433
+ */ getPriceFeedAccountAddress(shardId, priceFeedId) {
434
+ return getPriceFeedAccountForProgram(shardId, priceFeedId, this.pushOracle.programId);
435
+ }
436
+ /**
437
+ * Find all the encoded VAA accounts owned by this PythSolanaReceiver's wallet
438
+ * @returns a list of the public keys of the encoded VAA accounts
439
+ */ async findOwnedEncodedVaaAccounts() {
440
+ return await findEncodedVaaAccountsByWriteAuthority(this.receiver.provider.connection, this.wallet.publicKey, this.wormhole.programId);
441
+ }
442
+ }
443
+ /**
444
+ * Derive the address of a price feed account
445
+ * @param shardId The shard ID of the set of price feed accounts. This shard ID allows for multiple price feed accounts for the same price feed id to exist.
446
+ * @param priceFeedId The price feed ID, as either a 32-byte buffer or hexadecimal string with or without a leading "0x" prefix.
447
+ * @param pushOracleProgramId The program ID of the Pyth Push Oracle program. If not provided, the default deployment will be used.
448
+ * @returns The address of the price feed account
449
+ */ export function getPriceFeedAccountForProgram(shardId, priceFeedId, pushOracleProgramId) {
450
+ if (typeof priceFeedId == "string") {
451
+ priceFeedId = priceFeedId.startsWith("0x") ? IsomorphicBuffer.from(priceFeedId.slice(2), "hex") : IsomorphicBuffer.from(priceFeedId, "hex");
452
+ }
453
+ if (priceFeedId.length != 32) {
454
+ throw new Error("Feed ID should be 32 bytes long");
455
+ }
456
+ const shardBuffer = IsomorphicBuffer.alloc(2);
457
+ shardBuffer.writeUint16LE(shardId, 0);
458
+ return PublicKey.findProgramAddressSync([
459
+ shardBuffer,
460
+ priceFeedId
461
+ ], pushOracleProgramId ?? DEFAULT_PUSH_ORACLE_PROGRAM_ID)[0];
462
+ }
@@ -0,0 +1,29 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ /**
3
+ * The default Pyth Solana Receiver program ID.
4
+ * The program is deployed at this address on all SVM networks.
5
+ */
6
+ export declare const DEFAULT_RECEIVER_PROGRAM_ID: PublicKey;
7
+ /**
8
+ * The default Wormhole program ID.
9
+ * The program is deployed at this address on all SVM networks.
10
+ */
11
+ export declare const DEFAULT_WORMHOLE_PROGRAM_ID: PublicKey;
12
+ export declare const DEFAULT_PUSH_ORACLE_PROGRAM_ID: PublicKey;
13
+ /**
14
+ * Returns the address of a guardian set account from the Wormhole program.
15
+ */
16
+ export declare const getGuardianSetPda: (guardianSetIndex: number, wormholeProgramId: PublicKey) => PublicKey;
17
+ /**
18
+ * The Pyth Solana Receiver has one treasury account for each u8 `treasuryId`.
19
+ * This is meant to avoid write-locks on the treasury account by load-balancing the writes across multiple accounts.
20
+ */
21
+ export declare function getRandomTreasuryId(): number;
22
+ /**
23
+ * Returns the address of a treasury account from the Pyth Solana Receiver program.
24
+ */
25
+ export declare const getTreasuryPda: (treasuryId: number, receiverProgramId: PublicKey) => PublicKey;
26
+ /**
27
+ * Returns the address of the config account from the Pyth Solana Receiver program.
28
+ */
29
+ export declare const getConfigPda: (receiverProgramId: PublicKey) => PublicKey;
@@ -0,0 +1,45 @@
1
+ // eslint-disable-next-line unicorn/prefer-node-protocol
2
+ import { Buffer as IsomorphicBuffer } from "buffer";
3
+ import { PublicKey } from "@solana/web3.js";
4
+ /**
5
+ * The default Pyth Solana Receiver program ID.
6
+ * The program is deployed at this address on all SVM networks.
7
+ */ export const DEFAULT_RECEIVER_PROGRAM_ID = new PublicKey("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ");
8
+ /**
9
+ * The default Wormhole program ID.
10
+ * The program is deployed at this address on all SVM networks.
11
+ */ export const DEFAULT_WORMHOLE_PROGRAM_ID = new PublicKey("HDwcJBJXjL9FpJ7UBsYBtaDjsBUhuLCUYoz3zr8SWWaQ");
12
+ export const DEFAULT_PUSH_ORACLE_PROGRAM_ID = new PublicKey("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT");
13
+ /**
14
+ * Returns the address of a guardian set account from the Wormhole program.
15
+ */ export const getGuardianSetPda = (guardianSetIndex, wormholeProgramId)=>{
16
+ const guardianSetIndexBuf = IsomorphicBuffer.alloc(4);
17
+ guardianSetIndexBuf.writeUInt32BE(guardianSetIndex, 0);
18
+ return PublicKey.findProgramAddressSync([
19
+ IsomorphicBuffer.from("GuardianSet"),
20
+ guardianSetIndexBuf
21
+ ], wormholeProgramId)[0];
22
+ };
23
+ /**
24
+ * The Pyth Solana Receiver has one treasury account for each u8 `treasuryId`.
25
+ * This is meant to avoid write-locks on the treasury account by load-balancing the writes across multiple accounts.
26
+ */ export function getRandomTreasuryId() {
27
+ return Math.floor(Math.random() * 256);
28
+ }
29
+ /**
30
+ * Returns the address of a treasury account from the Pyth Solana Receiver program.
31
+ */ export const getTreasuryPda = (treasuryId, receiverProgramId)=>{
32
+ return PublicKey.findProgramAddressSync([
33
+ IsomorphicBuffer.from("treasury"),
34
+ IsomorphicBuffer.from([
35
+ treasuryId
36
+ ])
37
+ ], receiverProgramId)[0];
38
+ };
39
+ /**
40
+ * Returns the address of the config account from the Pyth Solana Receiver program.
41
+ */ export const getConfigPda = (receiverProgramId)=>{
42
+ return PublicKey.findProgramAddressSync([
43
+ IsomorphicBuffer.from("config")
44
+ ], receiverProgramId)[0];
45
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A hard-coded budget for the compute units required for the `verifyEncodedVaa` instruction in the Wormhole program.
3
+ */
4
+ export declare const VERIFY_ENCODED_VAA_COMPUTE_BUDGET = 350000;
5
+ /**
6
+ * A hard-coded budget for the compute units required for the `postUpdateAtomic` instruction in the Pyth Solana Receiver program.
7
+ */
8
+ export declare const POST_UPDATE_ATOMIC_COMPUTE_BUDGET = 170000;
9
+ /**
10
+ * A hard-coded budget for the compute units required for the `postUpdate` instruction in the Pyth Solana Receiver program.
11
+ */
12
+ export declare const POST_UPDATE_COMPUTE_BUDGET = 35000;
13
+ /**
14
+ * A hard-coded budget for the compute units required for the `updatePriceFeed` instruction in the Pyth Push Oracle program.
15
+ */
16
+ export declare const UPDATE_PRICE_FEED_COMPUTE_BUDGET = 55000;
17
+ /**
18
+ * A hard-coded budget for the compute units required for the `initEncodedVaa` instruction in the Wormhole program.
19
+ */
20
+ export declare const INIT_ENCODED_VAA_COMPUTE_BUDGET = 3000;
21
+ /**
22
+ * A hard-coded budget for the compute units required for the `writeEncodedVaa` instruction in the Wormhole program.
23
+ */
24
+ export declare const WRITE_ENCODED_VAA_COMPUTE_BUDGET = 3000;
25
+ /**
26
+ * A hard-coded budget for the compute units required for the `closeEncodedVaa` instruction in the Wormhole program.
27
+ */
28
+ export declare const CLOSE_ENCODED_VAA_COMPUTE_BUDGET = 30000;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A hard-coded budget for the compute units required for the `verifyEncodedVaa` instruction in the Wormhole program.
3
+ */ export const VERIFY_ENCODED_VAA_COMPUTE_BUDGET = 350_000;
4
+ /**
5
+ * A hard-coded budget for the compute units required for the `postUpdateAtomic` instruction in the Pyth Solana Receiver program.
6
+ */ export const POST_UPDATE_ATOMIC_COMPUTE_BUDGET = 170_000;
7
+ /**
8
+ * A hard-coded budget for the compute units required for the `postUpdate` instruction in the Pyth Solana Receiver program.
9
+ */ export const POST_UPDATE_COMPUTE_BUDGET = 35_000;
10
+ /**
11
+ * A hard-coded budget for the compute units required for the `updatePriceFeed` instruction in the Pyth Push Oracle program.
12
+ */ export const UPDATE_PRICE_FEED_COMPUTE_BUDGET = 55_000;
13
+ /**
14
+ * A hard-coded budget for the compute units required for the `initEncodedVaa` instruction in the Wormhole program.
15
+ */ export const INIT_ENCODED_VAA_COMPUTE_BUDGET = 3000;
16
+ /**
17
+ * A hard-coded budget for the compute units required for the `writeEncodedVaa` instruction in the Wormhole program.
18
+ */ export const WRITE_ENCODED_VAA_COMPUTE_BUDGET = 3000;
19
+ /**
20
+ * A hard-coded budget for the compute units required for the `closeEncodedVaa` instruction in the Wormhole program.
21
+ */ export const CLOSE_ENCODED_VAA_COMPUTE_BUDGET = 30_000;