Skip to content

Commit

Permalink
feat(solana-receiver-js-sdk): verify & post TWAPs (#2186)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
tejasbadadare and guibescos authored Dec 19, 2024
1 parent eda14ad commit 956f53e
Show file tree
Hide file tree
Showing 10 changed files with 965 additions and 146 deletions.
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions target_chains/solana/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,10 @@ pub fn process_write_encoded_vaa_and_post_price_update(
update_instructions,
&vec![payer, &price_update_keypair],
)?;

println!(
"Price update posted to account: {}",
price_update_keypair.pubkey()
);
Ok(price_update_keypair.pubkey())
}

Expand Down Expand Up @@ -483,7 +486,7 @@ pub fn process_write_encoded_vaa_and_post_twap_update(
)?;

// Transaction 3: Write remaining VAA data and verify both VAAs
let mut verify_instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(400_000)];
let mut verify_instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(850_000)];
verify_instructions.extend(write_remaining_data_and_verify_vaa_ixs(
&payer.pubkey(),
start_vaa,
Expand Down Expand Up @@ -518,6 +521,10 @@ pub fn process_write_encoded_vaa_and_post_twap_update(
post_instructions,
&vec![payer, &twap_update_keypair],
)?;
println!(
"TWAP update posted to account: {}",
twap_update_keypair.pubkey()
);

Ok(twap_update_keypair.pubkey())
}
Expand Down
11 changes: 11 additions & 0 deletions target_chains/solana/programs/pyth-solana-receiver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ pub mod pyth_solana_receiver {
pub fn reclaim_rent(_ctx: Context<ReclaimRent>) -> Result<()> {
Ok(())
}
pub fn reclaim_twap_rent(_ctx: Context<ReclaimTwapRent>) -> Result<()> {
Ok(())
}
}

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

#[derive(Accounts)]
pub struct ReclaimTwapRent<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut, close = payer, constraint = twap_update_account.write_authority == payer.key() @ ReceiverError::WrongWriteAuthority)]
pub twap_update_account: Account<'info, TwapUpdate>,
}

fn deserialize_guardian_set_checked(
account_info: &AccountInfo<'_>,
wormhole: &Pubkey,
Expand Down
44 changes: 44 additions & 0 deletions target_chains/solana/sdk/js/pyth_solana_receiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,50 @@ Price updates are relatively large and can take multiple transactions to post on
You can reduce the size of the transaction payload by using `addPostPartiallyVerifiedPriceUpdates` instead of `addPostPriceUpdates`.
This method does sacrifice some security however -- please see the method documentation for more details.

### Post a TWAP price update

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.

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.

```typescript
// Fetch the binary TWAP data from hermes or benchmarks. See Preliminaries section above for more info.
const binaryDataArray = ["UE5BV...khz609", "UE5BV...BAg8i6"];

// Pass `closeUpdateAccounts: true` to automatically close the TWAP update accounts
// after they're consumed
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
closeUpdateAccounts: false,
});

// Post the updates and calculate the TWAP
await transactionBuilder.addPostTwapUpdates(binaryDataArray);

// You can now use the TWAP prices in subsequent instructions
await transactionBuilder.addTwapConsumerInstructions(
async (
getTwapUpdateAccount: (priceFeedId: string) => PublicKey
): Promise<InstructionWithEphemeralSigners[]> => {
// Generate instructions here that use the TWAP updates posted above.
// getTwapUpdateAccount(<price feed id>) will give you the account for each TWAP update.
return [];
}
);

// Send the instructions in the builder in 1 or more transactions.
// The builder will pack the instructions into transactions automatically.
sendTransactions(
await transactionBuilder.buildVersionedTransactions({
computeUnitPriceMicroLamports: 100000,
tightComputeBudget: true,
}),
pythSolanaReceiver.connection,
pythSolanaReceiver.wallet
);
```

See `examples/post_twap_update.ts` for a runnable example of posting a TWAP price update.

### Get Instructions

The `PythTransactionBuilder` class used in the examples above helps craft transactions that update prices and then use them in successive instructions.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import { InstructionWithEphemeralSigners, PythSolanaReceiver } from "../";
import { Wallet } from "@coral-xyz/anchor";
import fs from "fs";
import os from "os";
import { HermesClient } from "@pythnetwork/hermes-client";
import { sendTransactions } from "@pythnetwork/solana-utils";

// Get price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable
const SOL_PRICE_FEED_ID =
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
const ETH_PRICE_FEED_ID =
"0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";

let keypairFile = "";
if (process.env["SOLANA_KEYPAIR"]) {
keypairFile = process.env["SOLANA_KEYPAIR"];
} else {
keypairFile = `${os.homedir()}/.config/solana/id.json`;
}

async function main() {
const connection = new Connection("https://api.devnet.solana.com");
const keypair = await loadKeypairFromFile(keypairFile);
console.log(
`Sending transactions from account: ${keypair.publicKey.toBase58()}`
);
const wallet = new Wallet(keypair);
const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet });

// Get the TWAP update from hermes
const twapUpdateData = await getTwapUpdateData();
console.log(`Posting TWAP update: ${twapUpdateData}`);

// Similar to price updates, we'll keep closeUpdateAccounts = false for easy exploration
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
closeUpdateAccounts: false,
});

// Post the TWAP updates to ephemeral accounts, one per price feed
await transactionBuilder.addPostTwapUpdates(twapUpdateData);
console.log(
"The SOL/USD TWAP update will get posted to:",
transactionBuilder.getTwapUpdateAccount(SOL_PRICE_FEED_ID).toBase58()
);

await transactionBuilder.addTwapConsumerInstructions(
async (
getTwapUpdateAccount: (priceFeedId: string) => PublicKey
): Promise<InstructionWithEphemeralSigners[]> => {
// You can generate instructions here that use the TWAP updates posted above.
// getTwapUpdateAccount(<price feed id>) will give you the account you need.
return [];
}
);

// Send the instructions in the builder in 1 or more transactions
sendTransactions(
await transactionBuilder.buildVersionedTransactions({
computeUnitPriceMicroLamports: 100000,
tightComputeBudget: true,
}),
pythSolanaReceiver.connection,
pythSolanaReceiver.wallet
);
}

// Fetch TWAP update data from Hermes
async function getTwapUpdateData() {
const hermesConnection = new HermesClient("https://hermes.pyth.network/", {});

// Request TWAP updates for the last hour (3600 seconds)
const response = await hermesConnection.getLatestTwapUpdates(
[SOL_PRICE_FEED_ID, ETH_PRICE_FEED_ID],
3600,
{ encoding: "base64" }
);

return response.binary.data;
}

// Load a solana keypair from an id.json file
async function loadKeypairFromFile(filePath: string): Promise<Keypair> {
try {
const keypairData = JSON.parse(
await fs.promises.readFile(filePath, "utf8")
);
return Keypair.fromSecretKey(Uint8Array.from(keypairData));
} catch (error) {
throw new Error(`Error loading keypair from file: ${error}`);
}
}

main();
4 changes: 2 additions & 2 deletions target_chains/solana/sdk/js/pyth_solana_receiver/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pythnetwork/pyth-solana-receiver",
"version": "0.8.2",
"version": "0.9.0",
"description": "Pyth solana receiver SDK",
"homepage": "https://pyth.network",
"main": "lib/index.js",
Expand Down Expand Up @@ -45,7 +45,7 @@
"dependencies": {
"@coral-xyz/anchor": "^0.29.0",
"@noble/hashes": "^1.4.0",
"@pythnetwork/price-service-sdk": ">=1.6.0",
"@pythnetwork/price-service-sdk": "workspace:*",
"@pythnetwork/solana-utils": "workspace:*",
"@solana/web3.js": "^1.90.0"
}
Expand Down
Loading

0 comments on commit 956f53e

Please sign in to comment.