Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
242 changes: 159 additions & 83 deletions target_chains/solana/sdk/js/pyth_solana_receiver/README.md
Original file line number Diff line number Diff line change
@@ -1,116 +1,192 @@
# Pyth Solana Receiver JS SDK

This is a Javascript SDK to interact with the Pyth Solana Receiver contract whose code lives [here](/target_chains/solana).
[@pythnetwork/pyth-solana-receiver](https://www.npmjs.com/package/@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.

It is available on [npm](https://www.npmjs.com/package/@pythnetwork/pyth-solana-receiver).
The code for the underlying Pyth Solana Receiver program lives [here](/target_chains/solana).

## Pull model
## Installation

The Pyth Solana Receiver allows users to consume Pyth price updates on a pull basis. This means that the user is responsible for submitting the price data on-chain whenever they want to interact with an app that requires a price update.
You can install the package using your favorite typescript version manager

Price updates get posted into price update accounts, owned by the Receiver contract. Once an update has been posted to a price update account, it can be used by anyone by simply passing the price update account as one of the accounts in a Solana instruction.
Price update accounts can be closed by whoever wrote them to recover the rent.
**NPM:** `npm install @pythnetwork/pyth-solana-receiver`

## Example use
**Yarn:** `yarn add @pythnetwork/pyth-solana-receiver`

```ts
import { Connection, PublicKey } from "@solana/web3.js";
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
## Preliminaries

### Accessing Pyth Prices

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](https://docs.pyth.network/price-feeds/pythnet-price-feeds/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](https://docs.pyth.network/price-feeds/api-instances-and-providers/hermes).
The [Price Service Client](https://github.com/pyth-network/pyth-crosschain/tree/main/price_service/client/js) can be used to access Hermes prices in a convenient way.
- [Benchmarks](https://docs.pyth.network/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.

### Price Feed IDs

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](https://pyth.network/developers/price-feed-ids).

### Pyth Solana Receiver

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](../../../../../../../price_pusher/) 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.

## Usage

The `PythSolanaReceiver` class is the main entrypoint for the SDK.
Instantiate it with a Solana web3 `Connection` and anchor `Wallet`:

```typescript
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
import { MyFirstPythApp, IDL } from "./idl/my_first_pyth_app";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import { Wallet } from "@coral-xyz/anchor";

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 a price update

Post an update to an ephemeral price update account:

const SOL_PRICE_FEED_ID =
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
const ETH_PRICE_FEED_ID =
"0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";
```ts
// 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);

const priceServiceConnection = new PriceServiceConnection(
"https://hermes.pyth.network/",
{ priceFeedRequestConfig: { binary: true } }
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 [];
}
);
const priceUpdateData = await priceServiceConnection.getLatestVaas([
SOL_PRICE_FEED_ID,
ETH_PRICE_FEED_ID,
]); // Fetch off-chain price update data

const myFirstPythApp = new Program<MyFirstPythApp>(
IDL as MyFirstPythApp,
MY_FIRST_PYTH_APP_PROGRAM_ID,
{}

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

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 a price feed account

Update the price feed account for shard id 1:

```typescript
// Fetch this from hermes or benchmarks. See Preliminaries section above for more info.
const priceUpdateData =
"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";

const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
await transactionBuilder.addPostPriceUpdates(priceUpdateData);
// 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[]> => {
return [
{
instruction: await myFirstPythApp.methods
.consume()
.accounts({
solPriceUpdate: getPriceUpdateAccount(SOL_PRICE_FEED_ID),
ethPriceUpdate: getPriceUpdateAccount(ETH_PRICE_FEED_ID),
})
.instruction(),
signers: [],
},
];
// 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.
await pythSolanaReceiver.provider.sendAll(
await transactionBuilder.buildVersionedTransactions({
computeUnitPriceMicroLamports: 1000000,
computeUnitPriceMicroLamports: 100000,
})
);
```

Alternatively you can use the instruction builder methods from `PythSolanaReceiver` :
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:

```ts
import { PublicKey } from "@solana/web3.js";
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
import { MyFirstPythApp, IDL } from "./idl/my_first_pyth_app";
```typescript
const solUsdPriceFeedAccount = pythSolanaReceiver
.getPriceFeedAccountAddress(1, SOL_PRICE_FEED_ID)
.toBase58();
```

const SOL_PRICE_FEED_ID =
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
const ETH_PRICE_FEED_ID =
"0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";
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.

const priceServiceConnection = new PriceServiceConnection(
"https://hermes.pyth.network/",
{ priceFeedRequestConfig: { binary: true } }
);
const priceUpdateData = await priceServiceConnection.getLatestVaas([
SOL_PRICE_FEED_ID,
ETH_PRICE_FEED_ID,
]); // Fetch off-chain price update data
See `examples/update_price_feed.ts` for a runnable example of updating a price feed.

const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet });
const { postInstructions, closeInstructions, priceFeedIdToPriceUpdateAccount } =
await pythSolanaReceiver.buildPostPriceUpdateInstructions(priceUpdateData); // Get instructions to post the price update data and to close the accounts later
### Partially verified price updates

const myFirstPythApp = new Program<MyFirstPythApp>(
IDL as MyFirstPythApp,
MY_FIRST_PYTH_APP_PROGRAM_ID,
{}
);
const consumerInstruction: InstructionWithEphemeralSigners = {
instruction: await myFirstPythApp.methods
.consume()
.accounts({
solPriceUpdate: priceFeedIdToPriceUpdateAccount[SOL_PRICE_FEED_ID],
ethPriceUpdate: priceFeedIdToPriceUpdateAccount[ETH_PRICE_FEED_ID],
})
.instruction(),
signers: [],
};

const transactions = pythSolanaReceiver.batchIntoVersionedTransactions(
[...postInstructions, consumerInstruction, ...closeInstructions],
{ computeUnitPriceMicroLamports: 1000000 }
); // Put all the instructions together
await pythSolanaReceiver.provider.sendAll(transactions);
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.

### Get Instructions

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.

## Examples

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:

1. Clone the `pyth-crosschain` git repo
2. Run `npm install` in the root of the repo
3. Run `npx lerna run build` anywhere in the repo
4. From the `pyth_solana_receiver` directory, run `npx 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:

```bash
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](https://docs.solanalabs.com/cli/install), the running `solana-keygen new`.
Loading