@pythnetwork/pyth-solana-receiver is a Typescript SDK for interacting with the Pyth Solana Receiver contract. The SDK enables users to construct transactions that post Pyth price updates to the Solana blockchain and use them in downstream applications.
The code for the underlying Pyth Solana Receiver program lives here.
You can install the package using your favorite typescript version manager
NPM: npm install @pythnetwork/pyth-solana-receiver
Yarn: yarn add @pythnetwork/pyth-solana-receiver
This SDK is designed to be used in combination with a source of Pyth pricing data. There are two different sources of pricing data that users can choose from.
-
Hermes is a webservice that provides HTTP and websocket endpoints for retrieving real-time Pyth prices.
The example code below uses the public Hermes instance hosted by the Pyth Data Association at
https://hermes.pyth.network/
. Hermes is also available from several infrastructure providers listed here. The Price Service Client can be used to access Hermes prices in a convenient way. - Benchmarks is a webservice that provides HTTP endpoints for accessing historical Pyth prices. This service can be used for applications that require prices from specific times in the past.
Both of these services return Pyth price updates, which are binary blobs of signed and timestamped prices. This SDK enables users to post price updates to the Solana blockchain, verify their validity, and consume them in downstream Solana applications.
Pyth identifies each pair of assets (e.g., BTC/USD) with a unique price feed id. The price feed id is a UUID written as a hexadecimal string. In order to get the price for a specific pair of assets, you will need its corresponding price feed id. You can look up all available price feed ids here.
The Pyth Solana Receiver provides two different methods for posting and using price updates.
First, a price update can be written to a price update account. Once the account has been written, other programs can read the Pyth price from the account by simply including it in their instruction. Price update accounts are ephemeral: they have an owner who can overwrite their contents or close the account. This method for using Pyth prices is a good fit for applications that need to use prices at specific timestamps (e.g., to settle a trade at a time).
Second, a price update can be written to a price feed account. A price feed account is designed to work similarly to a Pyth price feed -- it holds a sequence of price updates that move forward in time. Applications can therefore store the address of a price feed account and read its contents whenever they need a recent price for the feed. Price feed accounts have a fixed address derived from the feed id and a shard id. The shard id allows different applications to use different accounts for the same feed, thereby reducing the impact of solana congestion. This method of using Pyth prices is a good fit for applications that always want to use the most recent price. Additionally, the Price Scheduler can be used to continuously write fresh updates to a price feed account, freeing applications from worrying about writing their own updates.
This SDK provides methods for working with both types of accounts.
The PythSolanaReceiver
class is the main entrypoint for the SDK.
Instantiate it with a Solana web3 Connection
and anchor Wallet
:
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import { Wallet } from "@coral-xyz/anchor";
import { sendTransactions } from "@pythnetwork/solana-utils";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const wallet = new Wallet(
Keypair.fromSecretKey(/* <insert private key here> */)
);
const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet });
Post an update to an ephemeral price update account:
// Fetch this from hermes or benchmarks. See Preliminaries section above for more info.
const priceUpdateData =
"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";
// Pass `closeUpdateAccounts: true` to the transaction builder constructor to automatically close the
// price update accounts at the end of the sequence of transactions.
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
closeUpdateAccounts: false,
});
await transactionBuilder.addPostPriceUpdates(priceUpdateData);
await transactionBuilder.addPriceConsumerInstructions(
async (
getPriceUpdateAccount: (priceFeedId: string) => PublicKey
): Promise<InstructionWithEphemeralSigners[]> => {
// Generate instructions here that use the price updates posted above.
// getPriceUpdateAccount(<price feed id>) will give you the account for each price update.
return [];
}
);
// Send the instructions in the builder in 1 or more transactions.
// The builder will pack the instructions into transactions automatically.
// We use some custom transaction dispatch logic instead of the simple `provider.sendAll` to increase landing rate,
// feel free to use your own optimized logic.
sendTransactions(
await transactionBuilder.buildVersionedTransactions({
computeUnitPriceMicroLamports: 100000,
tightComputeBudget: true,
}),
pythSolanaReceiver.connection,
pythSolanaReceiver.wallet
);
The code snippet above will post every price update in priceUpdateData
to a new ephemeral account.
See examples/post_price_update.ts
for a runnable example of posting a price update.
Update the price feed account for shard id 1:
// Fetch this from hermes or benchmarks. See Preliminaries section above for more info.
const priceUpdateData =
"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";
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
// Update the price feed accounts for the feed ids in priceUpdateData and shard id 1
await transactionBuilder.addUpdatePriceFeed(priceUpdateData, 1);
await transactionBuilder.addPriceConsumerInstructions(
async (
getPriceUpdateAccount: (priceFeedId: string) => PublicKey
): Promise<InstructionWithEphemeralSigners[]> => {
// Generate instructions here that use the price updates posted above.
// getPriceUpdateAccount(<price feed id>) will give you the account for each price update.
return [];
}
);
// Send the instructions in the builder in 1 or more transactions.
// The builder will pack the instructions into transactions automatically.
// We use some custom transaction dispatch logic instead of the simple `provider.sendAll` to increase landing rate,
// feel free to use your own optimized logic.
sendTransactions(
await transactionBuilder.buildVersionedTransactions({
computeUnitPriceMicroLamports: 100000,
tightComputeBudget: true,
}),
pythSolanaReceiver.connection,
pythSolanaReceiver.wallet
);
The code above will update the price feed accounts for the feeds in priceUpdateData
(in this example, SOL/USD and ETH/USD).
The address of the price feed accounts can be derived automatically from the feed id and the shard id:
const solUsdPriceFeedAccount = pythSolanaReceiver
.getPriceFeedAccountAddress(1, SOL_PRICE_FEED_ID)
.toBase58();
Note that the example above uses a shard id of 1. Changing the shard id to a different value will give you a different account address.
See examples/update_price_feed.ts
for a runnable example of updating a price feed.
Price updates are relatively large and can take multiple transactions to post on the blockchain.
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.
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.
// 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.
The PythTransactionBuilder
class used in the examples above helps craft transactions that update prices and then use them in successive instructions.
However, if you would like to craft your own transactions, PythSolanaReceiver
exposes several methods for constructing the instructions for working with both price update accounts and price feed accounts.
See examples/post_price_update_instructions.ts
for an example of how to work with instructions.
This SDK includes several runnable examples in the examples/
directory.
You can run these examples by performing the following steps.
First, install and build any necessary typescript dependencies:
- Clone the
pyth-crosschain
git repo - Run
pnpm i
in the root of the repo - Run
pnpm turbo build
anywhere in the repo - From the
pyth_solana_receiver
directory, runpnpm exec ts-node examples/<example filename>.ts
The examples require a Solana keypair with SOL to send Solana transactions.
By default, the examples will use the same Solana keypair used by the Solana CLI (at ~/.config/solana/id.json
).
You can override this default by setting the SOLANA_KEYPAIR
environment variable:
export SOLANA_KEYPAIR=/path/to/keypair/id.json
If you do not have a Solana keypair, you can generate one by downloading and installing the Solana CLI, the running solana-keygen new
.