Skip to content
This repository has been archived by the owner on Jun 30, 2022. It is now read-only.

Commit

Permalink
Update client library (#13)
Browse files Browse the repository at this point in the history
* Helper method for pricing a base currency in a quote currency (#8)

* Add method for getting twap

* initial implementation, seems to work

* minor

* minor

* refactor

* found bad case

* use u128

* working on it

* clarify

* cleanup

* more cleanup

* pretty sure i need this

* better

* bad merge

* no println

* adding solana tx stuff

* change approach a bit

* this seems to work

* comment

* cleanup

* refactor

* refactor

* initial implementation of mul

* exponent

* tests for normalize

* tests for normalize

* negative numbers in div

* handle negative numbers

* comments

* stuff

* cleanup

* unused

* minor

Co-authored-by: Jayant Krishnamurthy <[email protected]>

* Instruction counts and optimizations (#9)

* instruction counts

* reduce normalize opcount

* instruction counts

* tests

Co-authored-by: Jayant Krishnamurthy <[email protected]>

* Docs and utilities (#12)

* uh oh

* docs

* fix error docs

Co-authored-by: Jayant Krishnamurthy <[email protected]>

* bump version number

* bump version number

* ignore more files

* docs

* remove mod

* checked ops

Co-authored-by: Jayant Krishnamurthy <[email protected]>
  • Loading branch information
jayantk and Jayant Krishnamurthy authored Jan 5, 2022
1 parent a648e10 commit 1c1c283
Show file tree
Hide file tree
Showing 12 changed files with 1,328 additions and 120 deletions.
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

0 comments on commit 1c1c283

Please sign in to comment.