Skip to content

Commit 956f53e

Browse files
feat(solana-receiver-js-sdk): verify & post TWAPs (#2186)
* feat: update IDL * feat: add postTwapUpdates function * refactor: clean up * feat: add priceFeedIdToTwapUpdateAccount and update docs * doc: update readme * Apply suggestions from code review Co-authored-by: guibescos <[email protected]> * refactor: address pr comments * refactor: extract shared VAA instruction building into `generateVaaInstructionGroups`, allowing for flexible ix ordering and batching while sharing core logic * refactor: keep buildCloseEncodedVaaInstruction in PythSolanaReceiver for backward compat * fix: update compute budget for postTwapUpdate based on devnet runs * fix: fix comment * fix: imports, compute budget * fix: add reclaimTwapRent to recv contract * fix(cli): increase compute budget to avoid serde issues, add print statements * Apply suggestions from code review Co-authored-by: guibescos <[email protected]> * doc: update docstring --------- Co-authored-by: guibescos <[email protected]>
1 parent eda14ad commit 956f53e

File tree

10 files changed

+965
-146
lines changed

10 files changed

+965
-146
lines changed

pnpm-lock.yaml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

target_chains/solana/cli/src/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,10 @@ pub fn process_write_encoded_vaa_and_post_price_update(
431431
update_instructions,
432432
&vec![payer, &price_update_keypair],
433433
)?;
434-
434+
println!(
435+
"Price update posted to account: {}",
436+
price_update_keypair.pubkey()
437+
);
435438
Ok(price_update_keypair.pubkey())
436439
}
437440

@@ -483,7 +486,7 @@ pub fn process_write_encoded_vaa_and_post_twap_update(
483486
)?;
484487

485488
// Transaction 3: Write remaining VAA data and verify both VAAs
486-
let mut verify_instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(400_000)];
489+
let mut verify_instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(850_000)];
487490
verify_instructions.extend(write_remaining_data_and_verify_vaa_ixs(
488491
&payer.pubkey(),
489492
start_vaa,
@@ -518,6 +521,10 @@ pub fn process_write_encoded_vaa_and_post_twap_update(
518521
post_instructions,
519522
&vec![payer, &twap_update_keypair],
520523
)?;
524+
println!(
525+
"TWAP update posted to account: {}",
526+
twap_update_keypair.pubkey()
527+
);
521528

522529
Ok(twap_update_keypair.pubkey())
523530
}

target_chains/solana/programs/pyth-solana-receiver/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ pub mod pyth_solana_receiver {
283283
pub fn reclaim_rent(_ctx: Context<ReclaimRent>) -> Result<()> {
284284
Ok(())
285285
}
286+
pub fn reclaim_twap_rent(_ctx: Context<ReclaimTwapRent>) -> Result<()> {
287+
Ok(())
288+
}
286289
}
287290

288291
#[derive(Accounts)]
@@ -393,6 +396,14 @@ pub struct ReclaimRent<'info> {
393396
pub price_update_account: Account<'info, PriceUpdateV2>,
394397
}
395398

399+
#[derive(Accounts)]
400+
pub struct ReclaimTwapRent<'info> {
401+
#[account(mut)]
402+
pub payer: Signer<'info>,
403+
#[account(mut, close = payer, constraint = twap_update_account.write_authority == payer.key() @ ReceiverError::WrongWriteAuthority)]
404+
pub twap_update_account: Account<'info, TwapUpdate>,
405+
}
406+
396407
fn deserialize_guardian_set_checked(
397408
account_info: &AccountInfo<'_>,
398409
wormhole: &Pubkey,

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 price 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 is the same as standard price updates. Get the binary update data from Hermes or Benchmarks, post and verify the VAAs via the Wormhole contract, and verify the updates against the VAAs via 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 binaryDataArray = ["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(binaryDataArray);
196+
197+
// You can now use the TWAP prices in subsequent instructions
198+
await transactionBuilder.addTwapConsumerInstructions(
199+
async (
200+
getTwapUpdateAccount: (priceFeedId: string) => PublicKey
201+
): Promise<InstructionWithEphemeralSigners[]> => {
202+
// Generate instructions here that use the TWAP updates posted above.
203+
// getTwapUpdateAccount(<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/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/pyth-solana-receiver",
3-
"version": "0.8.2",
3+
"version": "0.9.0",
44
"description": "Pyth solana receiver SDK",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",
@@ -45,7 +45,7 @@
4545
"dependencies": {
4646
"@coral-xyz/anchor": "^0.29.0",
4747
"@noble/hashes": "^1.4.0",
48-
"@pythnetwork/price-service-sdk": ">=1.6.0",
48+
"@pythnetwork/price-service-sdk": "workspace:*",
4949
"@pythnetwork/solana-utils": "workspace:*",
5050
"@solana/web3.js": "^1.90.0"
5151
}

0 commit comments

Comments
 (0)