Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(solana-receiver-js-sdk): verify & post TWAPs #2186

Merged
merged 20 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4451b45
feat: update IDL
tejasbadadare Dec 10, 2024
8458dc4
feat: add postTwapUpdates function
tejasbadadare Dec 11, 2024
e61f976
Merge branch 'main' of github.com:pyth-network/pyth-crosschain into t…
tejasbadadare Dec 11, 2024
9474df5
refactor: clean up
tejasbadadare Dec 12, 2024
5fbf06c
Merge branch 'main' of github.com:pyth-network/pyth-crosschain into t…
tejasbadadare Dec 12, 2024
e9445f5
feat: add priceFeedIdToTwapUpdateAccount and update docs
tejasbadadare Dec 12, 2024
1970a2a
doc: update readme
tejasbadadare Dec 12, 2024
920aa9c
Apply suggestions from code review
tejasbadadare Dec 17, 2024
faf0407
refactor: address pr comments
tejasbadadare Dec 17, 2024
82dbaba
Merge branch 'tb/solana-receiver-js-sdk/post-twap-updates' of github.…
tejasbadadare Dec 17, 2024
78aee7b
refactor: extract shared VAA instruction building into `generateVaaIn…
tejasbadadare Dec 17, 2024
2cb0b4b
refactor: keep buildCloseEncodedVaaInstruction in PythSolanaReceiver …
tejasbadadare Dec 17, 2024
e2df274
fix: update compute budget for postTwapUpdate based on devnet runs
tejasbadadare Dec 18, 2024
e4d4d1b
fix: fix comment
tejasbadadare Dec 18, 2024
2628133
fix: imports, compute budget
tejasbadadare Dec 18, 2024
7daa553
Merge branch 'main' of github.com:pyth-network/pyth-crosschain into t…
tejasbadadare Dec 18, 2024
36c21a2
fix: add reclaimTwapRent to recv contract
tejasbadadare Dec 18, 2024
4699780
fix(cli): increase compute budget to avoid serde issues, add print st…
tejasbadadare Dec 19, 2024
404c21a
Apply suggestions from code review
tejasbadadare Dec 19, 2024
200747f
doc: update docstring
tejasbadadare Dec 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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.
tejasbadadare marked this conversation as resolved.
Show resolved Hide resolved

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, 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.
tejasbadadare marked this conversation as resolved.
Show resolved Hide resolved

```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";
tejasbadadare marked this conversation as resolved.
Show resolved Hide resolved
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:*",
tejasbadadare marked this conversation as resolved.
Show resolved Hide resolved
"@pythnetwork/solana-utils": "workspace:*",
"@solana/web3.js": "^1.90.0"
}
Expand Down
Loading
Loading