Skip to content

Commit e9445f5

Browse files
committed
feat: add priceFeedIdToTwapUpdateAccount and update docs
1 parent 5fbf06c commit e9445f5

File tree

3 files changed

+196
-2
lines changed

3 files changed

+196
-2
lines changed

target_chains/solana/sdk/js/pyth_solana_receiver/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,50 @@ Price updates are relatively large and can take multiple transactions to post on
175175
You can reduce the size of the transaction payload by using `addPostPartiallyVerifiedPriceUpdates` instead of `addPostPriceUpdates`.
176176
This method does sacrifice some security however -- please see the method documentation for more details.
177177

178+
### Post a TWAP price update
179+
180+
TWAP prices updates are calculated using a pair of verifiable cumulative price updates per price feed (the "start" and "end" updates for the given time window), and then performing an averaging calculation on-chain to create the time-weighted average price.
181+
182+
The flow of using verifying, posting, and consuming these prices are the same as standard price updates. Get the binary update data from Hermes or Benchmarks, verify the VAAs via the Wormhole contract, and post the VAAs to the Pyth receiver contract. After this, you can consume the calculated TWAP posted to the TwapUpdate account. You can also optionally close these ephemeral accounts after the TWAP has been consumed to save on rent.
183+
184+
```typescript
185+
// Fetch the binary TWAP data from hermes or benchmarks. See Preliminaries section above for more info.
186+
const binary_data_array = ["UE5BV...khz609", "UE5BV...BAg8i6"];
187+
188+
// Pass `closeUpdateAccounts: true` to automatically close the TWAP update accounts
189+
// after they're consumed
190+
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
191+
closeUpdateAccounts: false,
192+
});
193+
194+
// Post the updates and calculate the TWAP
195+
await transactionBuilder.addPostTwapUpdates(binary_data_array);
196+
197+
// You can now use the TWAP prices in subsequent instructions
198+
await transactionBuilder.addPriceConsumerInstructions(
199+
async (
200+
getTwapUpdateAccount: (priceFeedId: string) => PublicKey
201+
): Promise<InstructionWithEphemeralSigners[]> => {
202+
// Generate instructions here that use the TWAP updates posted above.
203+
// getTwaoUpdateAccount(<price feed id>) will give you the account for each TWAP update.
204+
return [];
205+
}
206+
);
207+
208+
// Send the instructions in the builder in 1 or more transactions.
209+
// The builder will pack the instructions into transactions automatically.
210+
sendTransactions(
211+
await transactionBuilder.buildVersionedTransactions({
212+
computeUnitPriceMicroLamports: 100000,
213+
tightComputeBudget: true,
214+
}),
215+
pythSolanaReceiver.connection,
216+
pythSolanaReceiver.wallet
217+
);
218+
```
219+
220+
See `examples/post_twap_update.ts` for a runnable example of posting a TWAP price update.
221+
178222
### Get Instructions
179223

180224
The `PythTransactionBuilder` class used in the examples above helps craft transactions that update prices and then use them in successive instructions.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
2+
import { InstructionWithEphemeralSigners, PythSolanaReceiver } from "../";
3+
import { Wallet } from "@coral-xyz/anchor";
4+
import fs from "fs";
5+
import os from "os";
6+
import { HermesClient } from "@pythnetwork/hermes-client";
7+
import { sendTransactions } from "@pythnetwork/solana-utils";
8+
9+
// Get price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable
10+
const SOL_PRICE_FEED_ID =
11+
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
12+
const ETH_PRICE_FEED_ID =
13+
"0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";
14+
15+
let keypairFile = "";
16+
if (process.env["SOLANA_KEYPAIR"]) {
17+
keypairFile = process.env["SOLANA_KEYPAIR"];
18+
} else {
19+
keypairFile = `${os.homedir()}/.config/solana/id.json`;
20+
}
21+
22+
async function main() {
23+
const connection = new Connection("https://api.devnet.solana.com");
24+
const keypair = await loadKeypairFromFile(keypairFile);
25+
console.log(
26+
`Sending transactions from account: ${keypair.publicKey.toBase58()}`
27+
);
28+
const wallet = new Wallet(keypair);
29+
const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet });
30+
31+
// Get the TWAP update from hermes
32+
const twapUpdateData = await getTwapUpdateData();
33+
console.log(`Posting TWAP update: ${twapUpdateData}`);
34+
35+
// Similar to price updates, we'll keep closeUpdateAccounts = false for easy exploration
36+
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
37+
closeUpdateAccounts: false,
38+
});
39+
40+
// Post the TWAP updates to ephemeral accounts, one per price feed
41+
await transactionBuilder.addPostTwapUpdates(twapUpdateData);
42+
console.log(
43+
"The SOL/USD TWAP update will get posted to:",
44+
transactionBuilder.getTwapUpdateAccount(SOL_PRICE_FEED_ID).toBase58()
45+
);
46+
47+
await transactionBuilder.addTwapConsumerInstructions(
48+
async (
49+
getTwapUpdateAccount: (priceFeedId: string) => PublicKey
50+
): Promise<InstructionWithEphemeralSigners[]> => {
51+
// You can generate instructions here that use the TWAP updates posted above.
52+
// getTwapUpdateAccount(<price feed id>) will give you the account you need.
53+
return [];
54+
}
55+
);
56+
57+
// Send the instructions in the builder in 1 or more transactions
58+
sendTransactions(
59+
await transactionBuilder.buildVersionedTransactions({
60+
computeUnitPriceMicroLamports: 100000,
61+
tightComputeBudget: true,
62+
}),
63+
pythSolanaReceiver.connection,
64+
pythSolanaReceiver.wallet
65+
);
66+
}
67+
68+
// Fetch TWAP update data from Hermes
69+
async function getTwapUpdateData() {
70+
const hermesConnection = new HermesClient("https://hermes.pyth.network/", {});
71+
72+
// Request TWAP updates for the last hour (3600 seconds)
73+
const response = await hermesConnection.getLatestTwapUpdates(
74+
[SOL_PRICE_FEED_ID, ETH_PRICE_FEED_ID],
75+
3600,
76+
{ encoding: "base64" }
77+
);
78+
79+
return response.binary.data;
80+
}
81+
82+
// Load a solana keypair from an id.json file
83+
async function loadKeypairFromFile(filePath: string): Promise<Keypair> {
84+
try {
85+
const keypairData = JSON.parse(
86+
await fs.promises.readFile(filePath, "utf8")
87+
);
88+
return Keypair.fromSecretKey(Uint8Array.from(keypairData));
89+
} catch (error) {
90+
throw new Error(`Error loading keypair from file: ${error}`);
91+
}
92+
}
93+
94+
main();

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export class PythTransactionBuilder extends TransactionBuilder {
103103
readonly pythSolanaReceiver: PythSolanaReceiver;
104104
readonly closeInstructions: InstructionWithEphemeralSigners[];
105105
readonly priceFeedIdToPriceUpdateAccount: Record<string, PublicKey>;
106+
readonly priceFeedIdToTwapUpdateAccount: Record<string, PublicKey>;
106107
readonly closeUpdateAccounts: boolean;
107108

108109
constructor(
@@ -118,6 +119,7 @@ export class PythTransactionBuilder extends TransactionBuilder {
118119
this.pythSolanaReceiver = pythSolanaReceiver;
119120
this.closeInstructions = [];
120121
this.priceFeedIdToPriceUpdateAccount = {};
122+
this.priceFeedIdToTwapUpdateAccount = {};
121123
this.closeUpdateAccounts = config.closeUpdateAccounts ?? true;
122124
}
123125

@@ -227,7 +229,7 @@ export class PythTransactionBuilder extends TransactionBuilder {
227229
);
228230
this.closeInstructions.push(...closeInstructions);
229231
Object.assign(
230-
this.priceFeedIdToPriceUpdateAccount,
232+
this.priceFeedIdToTwapUpdateAccount,
231233
priceFeedIdToTwapUpdateAccount
232234
);
233235
this.addInstructions(postInstructions);
@@ -311,6 +313,46 @@ export class PythTransactionBuilder extends TransactionBuilder {
311313
);
312314
}
313315

316+
/**
317+
* Add instructions that consume TWAP updates to the builder.
318+
*
319+
* @param getInstructions a function that given a mapping of price feed IDs to TWAP update accounts, generates a series of instructions. TWAP 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.
320+
* If multiple TWAP 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.
321+
*
322+
* @example
323+
* ```typescript
324+
* ...
325+
* await transactionBuilder.addPostTwapUpdates(twapUpdateData);
326+
* await transactionBuilder.addTwapConsumerInstructions(
327+
* async (
328+
* getTwapUpdateAccount: ( priceFeedId: string) => PublicKey
329+
* ): Promise<InstructionWithEphemeralSigners[]> => {
330+
* return [
331+
* {
332+
* instruction: await myFirstPythApp.methods
333+
* .consume()
334+
* .accounts({
335+
* solTwapUpdate: getTwapUpdateAccount(SOL_PRICE_FEED_ID),
336+
* ethTwapUpdate: getTwapUpdateAccount(ETH_PRICE_FEED_ID),
337+
* })
338+
* .instruction(),
339+
* signers: [],
340+
* },
341+
* ];
342+
* }
343+
* );
344+
* ```
345+
*/
346+
async addTwapConsumerInstructions(
347+
getInstructions: (
348+
getTwapUpdateAccount: (priceFeedId: string) => PublicKey
349+
) => Promise<InstructionWithEphemeralSigners[]>
350+
) {
351+
this.addInstructions(
352+
await getInstructions(this.getTwapUpdateAccount.bind(this))
353+
);
354+
}
355+
314356
/** Add instructions to close encoded VAA accounts from previous actions.
315357
* 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.
316358
* 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.
@@ -356,11 +398,25 @@ export class PythTransactionBuilder extends TransactionBuilder {
356398
this.priceFeedIdToPriceUpdateAccount[priceFeedId];
357399
if (!priceUpdateAccount) {
358400
throw new Error(
359-
`No price update account found for the price feed ID ${priceFeedId}. Make sure to call addPostPriceUpdates or addPostPartiallyVerifiedPriceUpdates or postTwapUpdates before calling this function.`
401+
`No price update account found for the price feed ID ${priceFeedId}. Make sure to call addPostPriceUpdates or addPostPartiallyVerifiedPriceUpdates before calling this function.`
360402
);
361403
}
362404
return priceUpdateAccount;
363405
}
406+
407+
/**
408+
* This method is used to retrieve the address of the TWAP update account where the TWAP update for a given price feed ID will be posted.
409+
* If multiple 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.
410+
* */
411+
getTwapUpdateAccount(priceFeedId: string): PublicKey {
412+
const twapUpdateAccount = this.priceFeedIdToTwapUpdateAccount[priceFeedId];
413+
if (!twapUpdateAccount) {
414+
throw new Error(
415+
`No TWAP update account found for the price feed ID ${priceFeedId}. Make sure to call addPostTwapUpdates before calling this function.`
416+
);
417+
}
418+
return twapUpdateAccount;
419+
}
364420
}
365421

366422
/**

0 commit comments

Comments
 (0)