Skip to content
This repository was archived by the owner on Jun 30, 2022. It is now read-only.
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
debug
target
Cargo.lock

# IntelliJ temp files
.idea
*.iml
27 changes: 22 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyth-client"
version = "0.2.3-beta.0"
version = "0.3.0"
authors = ["Richard Brooks"]
edition = "2018"
license = "Apache-2.0"
Expand All @@ -10,10 +10,27 @@ description = "pyth price oracle data structures and example usage"
keywords = [ "pyth", "solana", "oracle" ]
readme = "README.md"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
test-bpf = []
no-entrypoint = []

[dependencies]
solana-program = "1.8.1"
borsh = "0.9"
borsh-derive = "0.9.0"
bytemuck = "1.7.2"
num-derive = "0.3"
num-traits = "0.2"
thiserror = "1.0"

[dev-dependencies]
solana-client = "1.6.7"
solana-sdk = "1.6.7"
solana-program = "1.6.7"
solana-program-test = "1.8.1"
solana-client = "1.8.1"
solana-sdk = "1.8.1"

[lib]
crate-type = ["cdylib", "lib"]

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

130 changes: 125 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,110 @@
# pyth-client-rs
# Pyth Client

A rust API for desribing on-chain pyth account structures. A primer on pyth accounts can be found at https://github.com/pyth-network/pyth-client/blob/main/doc/aggregate_price.md
This crate provides utilities for reading price feeds from the [pyth.network](https://pyth.network/) oracle on the Solana network.
The crate includes a library for on-chain programs and an off-chain example program.

Key features of this library include:

Contains a library for use in on-chain program development and an off-chain example program for loading and printing product reference data and aggregate prices from all devnet pyth accounts.
* Get the current price of over [50 products](https://pyth.network/markets/), including cryptocurrencies,
US equities, forex and more.
* Combine listed products to create new price feeds, e.g., for baskets of tokens or non-USD quote currencies.
* Consume prices in on-chain Solana programs or off-chain applications.

### Running the Example
Please see the [pyth.network documentation](https://docs.pyth.network/) for more information about pyth.network.

## Installation

Add a dependency to your Cargo.toml:

```toml
[dependencies]
pyth-client="<version>"
```

See [pyth-client on crates.io](https://crates.io/crates/pyth-client/) to get the latest version of the library.

## Usage

Pyth Network stores its price feeds in a collection of Solana accounts.
This crate provides utilities for interpreting and manipulating the content of these accounts.
Applications can obtain the content of these accounts in two different ways:
* On-chain programs should pass these accounts to the instructions that require price feeds.
* Off-chain programs can access these accounts using the Solana RPC client (as in the [example program](examples/get_accounts.rs)).

In both cases, the content of the account will be provided to the application as a binary blob (`Vec<u8>`).
The examples below assume that the user has already obtained this account data.

### Parse account data

Pyth Network has several different types of accounts:
* Price accounts store the current price for a product
* Product accounts store metadata about a product, such as its symbol (e.g., "BTC/USD").
* Mapping accounts store a listing of all Pyth accounts

For more information on the different types of Pyth accounts, see the [account structure documentation](https://docs.pyth.network/how-pyth-works/account-structure).
The pyth.network website also lists the public keys of the accounts (e.g., [BTC/USD accounts](https://pyth.network/markets/#BTC/USD)).

This library provides several `load_*` methods that translate the binary data in each account into an appropriate struct:

```rust
// replace with account data, either passed to on-chain program or from RPC node
let price_account_data: Vec<u8> = ...;
let price_account: Price = load_price( &price_account_data ).unwrap();

let product_account_data: Vec<u8> = ...;
let product_account: Product = load_product( &product_account_data ).unwrap();

let mapping_account_data: Vec<u8> = ...;
let mapping_account: Mapping = load_mapping( &mapping_account_data ).unwrap();
```

### Get the current price

Read the current price from a `Price` account:

```rust
let price: PriceConf = price_account.get_current_price().unwrap();
println!("price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo);
```

The price is returned along with a confidence interval that represents the degree of uncertainty in the price.
Both values are represented as fixed-point numbers, `a * 10^e`.
The method will return `None` if the price is not currently available.

### Non-USD prices

Most assets in Pyth are priced in USD.
Applications can combine two USD prices to price an asset in a different quote currency:

```rust
let btc_usd: Price = ...;
let eth_usd: Price = ...;
// -8 is the desired exponent for the result
let btc_eth: PriceConf = btc_usd.get_price_in_quote(&eth_usd, -8);
println!("BTC/ETH price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo);
```

### Price a basket of assets

Applications can also compute the value of a basket of multiple assets:

```rust
let btc_usd: Price = ...;
let eth_usd: Price = ...;
// Quantity of each asset in fixed-point a * 10^e.
// This represents 0.1 BTC and .05 ETH.
// -8 is desired exponent for result
let basket_price: PriceConf = Price::price_basket(&[
(btc_usd, 10, -2),
(eth_usd, 5, -2)
], -8);
println!("0.1 BTC and 0.05 ETH are worth: ({} +- {}) x 10^{} USD",
basket_price.price, basket_price.conf, basket_price.expo);
```

This function additionally propagates any uncertainty in the price into uncertainty in the value of the basket.

### Off-chain example program

The example program prints the product reference data and current price information for Pyth on Solana devnet.
Run the following commands to try this example program:
Expand Down Expand Up @@ -37,4 +136,25 @@ product_account .. 6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8
publish_slot . 91340925
twap ......... 7426390900
twac ......... 2259870
```
```

## Development

This library can be built for either your native platform or in BPF (used by Solana programs).
Use `cargo build` / `cargo test` to build and test natively.
Use `cargo build-bpf` / `cargo test-bpf` to build in BPF for Solana; these commands require you to have installed the [Solana CLI tools](https://docs.solana.com/cli/install-solana-cli-tools).

The BPF tests will also run an instruction count program that logs the resource consumption
of various library functions.
This program can also be run on its own using `cargo test-bpf --test instruction_count`.

### Releases

To release a new version of this package, perform the following steps:

1. Increment the version number in `Cargo.toml`.
You may use a version number with a `-beta.x` suffix such as `0.0.1-beta.0` to create opt-in test versions.
2. Merge your change into `main` on github.
3. Create and publish a new github release.
The name of the release should be the version number, and the tag should be the version number prefixed with `v`.
Publishing the release will trigger a github action that will automatically publish the [pyth-client](https://crates.io/crates/pyth-client) rust crate to `crates.io`.
2 changes: 2 additions & 0 deletions Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
45 changes: 13 additions & 32 deletions examples/get_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@
// bootstrap all product and pricing accounts from root mapping account

use pyth_client::{
AccountType,
Mapping,
Product,
Price,
PriceType,
PriceStatus,
CorpAction,
cast,
MAGIC,
VERSION_2,
load_mapping,
load_product,
load_price,
PROD_HDR_SIZE
};
use solana_client::{
Expand Down Expand Up @@ -71,24 +67,14 @@ fn main() {
loop {
// get Mapping account from key
let map_data = clnt.get_account_data( &akey ).unwrap();
let map_acct = cast::<Mapping>( &map_data );
assert_eq!( map_acct.magic, MAGIC, "not a valid pyth account" );
assert_eq!( map_acct.atype, AccountType::Mapping as u32,
"not a valid pyth mapping account" );
assert_eq!( map_acct.ver, VERSION_2,
"unexpected pyth mapping account version" );
let map_acct = load_mapping( &map_data ).unwrap();

// iget and print each Product in Mapping directory
let mut i = 0;
for prod_akey in &map_acct.products {
let prod_pkey = Pubkey::new( &prod_akey.val );
let prod_data = clnt.get_account_data( &prod_pkey ).unwrap();
let prod_acct = cast::<Product>( &prod_data );
assert_eq!( prod_acct.magic, MAGIC, "not a valid pyth account" );
assert_eq!( prod_acct.atype, AccountType::Product as u32,
"not a valid pyth product account" );
assert_eq!( prod_acct.ver, VERSION_2,
"unexpected pyth product account version" );
let prod_acct = load_product( &prod_data ).unwrap();

// print key and reference data for this Product
println!( "product_account .. {:?}", prod_pkey );
Expand All @@ -106,20 +92,15 @@ fn main() {
let mut px_pkey = Pubkey::new( &prod_acct.px_acc.val );
loop {
let pd = clnt.get_account_data( &px_pkey ).unwrap();
let pa = cast::<Price>( &pd );
let pa = load_price( &pd ).unwrap();

assert_eq!( pa.magic, MAGIC, "not a valid pyth account" );
assert_eq!( pa.atype, AccountType::Price as u32,
"not a valid pyth price account" );
assert_eq!( pa.ver, VERSION_2,
"unexpected pyth price account version" );
println!( " price_account .. {:?}", px_pkey );

let maybe_price = pa.get_current_price();
match maybe_price {
Some((price, confidence, expo)) => {
println!(" price ........ {} x 10^{}", price, expo);
println!(" conf ......... {} x 10^{}", confidence, expo);
Some(p) => {
println!(" price ........ {} x 10^{}", p.price, p.expo);
println!(" conf ......... {} x 10^{}", p.conf, p.expo);
}
None => {
println!(" price ........ unavailable");
Expand All @@ -138,16 +119,16 @@ fn main() {

let maybe_twap = pa.get_twap();
match maybe_twap {
Some((twap, expo)) => {
println!( " twap ......... {} x 10^{}", twap, expo );
Some(twap) => {
println!( " twap ......... {} x 10^{}", twap.price, twap.expo );
println!( " twac ......... {} x 10^{}", twap.conf, twap.expo );
}
None => {
println!( " twap ......... unavailable");
println!( " twac ......... unavailable");
}
}

println!( " twac ......... {}", pa.twac.val );

// go to next price account in list
if pa.next.is_valid() {
px_pkey = Pubkey::new( &pa.next.val );
Expand Down
16 changes: 16 additions & 0 deletions src/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! Program entrypoint

#![cfg(not(feature = "no-entrypoint"))]

use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
};

entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
crate::processor::process_instruction(program_id, accounts, instruction_data)
}
25 changes: 25 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use num_derive::FromPrimitive;
use solana_program::program_error::ProgramError;
use thiserror::Error;

/// Errors that may be returned by Pyth.
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum PythError {
// 0
/// Invalid account data -- either insufficient data, or incorrect magic number
#[error("Failed to convert account into a Pyth account")]
InvalidAccountData,
/// Wrong version number
#[error("Incorrect version number for Pyth account")]
BadVersionNumber,
/// Tried reading an account with the wrong type, e.g., tried to read
/// a price account as a product account.
#[error("Incorrect account type")]
WrongAccountType,
}

impl From<PythError> for ProgramError {
fn from(e: PythError) -> Self {
ProgramError::Custom(e as u32)
}
}
Loading