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

Commit 1c1c283

Browse files
jayantkJayant Krishnamurthy
and
Jayant Krishnamurthy
authored
Update client library (#13)
* 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]>
1 parent a648e10 commit 1c1c283

12 files changed

+1328
-120
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
debug
33
target
44
Cargo.lock
5+
6+
# IntelliJ temp files
7+
.idea
8+
*.iml

Cargo.toml

+22-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-client"
3-
version = "0.2.3-beta.0"
3+
version = "0.3.0"
44
authors = ["Richard Brooks"]
55
edition = "2018"
66
license = "Apache-2.0"
@@ -10,10 +10,27 @@ description = "pyth price oracle data structures and example usage"
1010
keywords = [ "pyth", "solana", "oracle" ]
1111
readme = "README.md"
1212

13-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13+
[features]
14+
test-bpf = []
15+
no-entrypoint = []
16+
17+
[dependencies]
18+
solana-program = "1.8.1"
19+
borsh = "0.9"
20+
borsh-derive = "0.9.0"
21+
bytemuck = "1.7.2"
22+
num-derive = "0.3"
23+
num-traits = "0.2"
24+
thiserror = "1.0"
1425

1526
[dev-dependencies]
16-
solana-client = "1.6.7"
17-
solana-sdk = "1.6.7"
18-
solana-program = "1.6.7"
27+
solana-program-test = "1.8.1"
28+
solana-client = "1.8.1"
29+
solana-sdk = "1.8.1"
30+
31+
[lib]
32+
crate-type = ["cdylib", "lib"]
33+
34+
[package.metadata.docs.rs]
35+
targets = ["x86_64-unknown-linux-gnu"]
1936

README.md

+125-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,110 @@
1-
# pyth-client-rs
1+
# Pyth Client
22

3-
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
3+
This crate provides utilities for reading price feeds from the [pyth.network](https://pyth.network/) oracle on the Solana network.
4+
The crate includes a library for on-chain programs and an off-chain example program.
45

6+
Key features of this library include:
57

6-
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.
8+
* Get the current price of over [50 products](https://pyth.network/markets/), including cryptocurrencies,
9+
US equities, forex and more.
10+
* Combine listed products to create new price feeds, e.g., for baskets of tokens or non-USD quote currencies.
11+
* Consume prices in on-chain Solana programs or off-chain applications.
712

8-
### Running the Example
13+
Please see the [pyth.network documentation](https://docs.pyth.network/) for more information about pyth.network.
14+
15+
## Installation
16+
17+
Add a dependency to your Cargo.toml:
18+
19+
```toml
20+
[dependencies]
21+
pyth-client="<version>"
22+
```
23+
24+
See [pyth-client on crates.io](https://crates.io/crates/pyth-client/) to get the latest version of the library.
25+
26+
## Usage
27+
28+
Pyth Network stores its price feeds in a collection of Solana accounts.
29+
This crate provides utilities for interpreting and manipulating the content of these accounts.
30+
Applications can obtain the content of these accounts in two different ways:
31+
* On-chain programs should pass these accounts to the instructions that require price feeds.
32+
* Off-chain programs can access these accounts using the Solana RPC client (as in the [example program](examples/get_accounts.rs)).
33+
34+
In both cases, the content of the account will be provided to the application as a binary blob (`Vec<u8>`).
35+
The examples below assume that the user has already obtained this account data.
36+
37+
### Parse account data
38+
39+
Pyth Network has several different types of accounts:
40+
* Price accounts store the current price for a product
41+
* Product accounts store metadata about a product, such as its symbol (e.g., "BTC/USD").
42+
* Mapping accounts store a listing of all Pyth accounts
43+
44+
For more information on the different types of Pyth accounts, see the [account structure documentation](https://docs.pyth.network/how-pyth-works/account-structure).
45+
The pyth.network website also lists the public keys of the accounts (e.g., [BTC/USD accounts](https://pyth.network/markets/#BTC/USD)).
46+
47+
This library provides several `load_*` methods that translate the binary data in each account into an appropriate struct:
48+
49+
```rust
50+
// replace with account data, either passed to on-chain program or from RPC node
51+
let price_account_data: Vec<u8> = ...;
52+
let price_account: Price = load_price( &price_account_data ).unwrap();
53+
54+
let product_account_data: Vec<u8> = ...;
55+
let product_account: Product = load_product( &product_account_data ).unwrap();
56+
57+
let mapping_account_data: Vec<u8> = ...;
58+
let mapping_account: Mapping = load_mapping( &mapping_account_data ).unwrap();
59+
```
60+
61+
### Get the current price
62+
63+
Read the current price from a `Price` account:
64+
65+
```rust
66+
let price: PriceConf = price_account.get_current_price().unwrap();
67+
println!("price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo);
68+
```
69+
70+
The price is returned along with a confidence interval that represents the degree of uncertainty in the price.
71+
Both values are represented as fixed-point numbers, `a * 10^e`.
72+
The method will return `None` if the price is not currently available.
73+
74+
### Non-USD prices
75+
76+
Most assets in Pyth are priced in USD.
77+
Applications can combine two USD prices to price an asset in a different quote currency:
78+
79+
```rust
80+
let btc_usd: Price = ...;
81+
let eth_usd: Price = ...;
82+
// -8 is the desired exponent for the result
83+
let btc_eth: PriceConf = btc_usd.get_price_in_quote(&eth_usd, -8);
84+
println!("BTC/ETH price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo);
85+
```
86+
87+
### Price a basket of assets
88+
89+
Applications can also compute the value of a basket of multiple assets:
90+
91+
```rust
92+
let btc_usd: Price = ...;
93+
let eth_usd: Price = ...;
94+
// Quantity of each asset in fixed-point a * 10^e.
95+
// This represents 0.1 BTC and .05 ETH.
96+
// -8 is desired exponent for result
97+
let basket_price: PriceConf = Price::price_basket(&[
98+
(btc_usd, 10, -2),
99+
(eth_usd, 5, -2)
100+
], -8);
101+
println!("0.1 BTC and 0.05 ETH are worth: ({} +- {}) x 10^{} USD",
102+
basket_price.price, basket_price.conf, basket_price.expo);
103+
```
104+
105+
This function additionally propagates any uncertainty in the price into uncertainty in the value of the basket.
106+
107+
### Off-chain example program
9108

10109
The example program prints the product reference data and current price information for Pyth on Solana devnet.
11110
Run the following commands to try this example program:
@@ -37,4 +136,25 @@ product_account .. 6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8
37136
publish_slot . 91340925
38137
twap ......... 7426390900
39138
twac ......... 2259870
40-
```
139+
```
140+
141+
## Development
142+
143+
This library can be built for either your native platform or in BPF (used by Solana programs).
144+
Use `cargo build` / `cargo test` to build and test natively.
145+
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).
146+
147+
The BPF tests will also run an instruction count program that logs the resource consumption
148+
of various library functions.
149+
This program can also be run on its own using `cargo test-bpf --test instruction_count`.
150+
151+
### Releases
152+
153+
To release a new version of this package, perform the following steps:
154+
155+
1. Increment the version number in `Cargo.toml`.
156+
You may use a version number with a `-beta.x` suffix such as `0.0.1-beta.0` to create opt-in test versions.
157+
2. Merge your change into `main` on github.
158+
3. Create and publish a new github release.
159+
The name of the release should be the version number, and the tag should be the version number prefixed with `v`.
160+
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`.

Xargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.bpfel-unknown-unknown.dependencies.std]
2+
features = []

examples/get_accounts.rs

+13-32
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,12 @@
22
// bootstrap all product and pricing accounts from root mapping account
33

44
use pyth_client::{
5-
AccountType,
6-
Mapping,
7-
Product,
8-
Price,
95
PriceType,
106
PriceStatus,
117
CorpAction,
12-
cast,
13-
MAGIC,
14-
VERSION_2,
8+
load_mapping,
9+
load_product,
10+
load_price,
1511
PROD_HDR_SIZE
1612
};
1713
use solana_client::{
@@ -71,24 +67,14 @@ fn main() {
7167
loop {
7268
// get Mapping account from key
7369
let map_data = clnt.get_account_data( &akey ).unwrap();
74-
let map_acct = cast::<Mapping>( &map_data );
75-
assert_eq!( map_acct.magic, MAGIC, "not a valid pyth account" );
76-
assert_eq!( map_acct.atype, AccountType::Mapping as u32,
77-
"not a valid pyth mapping account" );
78-
assert_eq!( map_acct.ver, VERSION_2,
79-
"unexpected pyth mapping account version" );
70+
let map_acct = load_mapping( &map_data ).unwrap();
8071

8172
// iget and print each Product in Mapping directory
8273
let mut i = 0;
8374
for prod_akey in &map_acct.products {
8475
let prod_pkey = Pubkey::new( &prod_akey.val );
8576
let prod_data = clnt.get_account_data( &prod_pkey ).unwrap();
86-
let prod_acct = cast::<Product>( &prod_data );
87-
assert_eq!( prod_acct.magic, MAGIC, "not a valid pyth account" );
88-
assert_eq!( prod_acct.atype, AccountType::Product as u32,
89-
"not a valid pyth product account" );
90-
assert_eq!( prod_acct.ver, VERSION_2,
91-
"unexpected pyth product account version" );
77+
let prod_acct = load_product( &prod_data ).unwrap();
9278

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

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

11899
let maybe_price = pa.get_current_price();
119100
match maybe_price {
120-
Some((price, confidence, expo)) => {
121-
println!(" price ........ {} x 10^{}", price, expo);
122-
println!(" conf ......... {} x 10^{}", confidence, expo);
101+
Some(p) => {
102+
println!(" price ........ {} x 10^{}", p.price, p.expo);
103+
println!(" conf ......... {} x 10^{}", p.conf, p.expo);
123104
}
124105
None => {
125106
println!(" price ........ unavailable");
@@ -138,16 +119,16 @@ fn main() {
138119

139120
let maybe_twap = pa.get_twap();
140121
match maybe_twap {
141-
Some((twap, expo)) => {
142-
println!( " twap ......... {} x 10^{}", twap, expo );
122+
Some(twap) => {
123+
println!( " twap ......... {} x 10^{}", twap.price, twap.expo );
124+
println!( " twac ......... {} x 10^{}", twap.conf, twap.expo );
143125
}
144126
None => {
145127
println!( " twap ......... unavailable");
128+
println!( " twac ......... unavailable");
146129
}
147130
}
148131

149-
println!( " twac ......... {}", pa.twac.val );
150-
151132
// go to next price account in list
152133
if pa.next.is_valid() {
153134
px_pkey = Pubkey::new( &pa.next.val );

src/entrypoint.rs

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//! Program entrypoint
2+
3+
#![cfg(not(feature = "no-entrypoint"))]
4+
5+
use solana_program::{
6+
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
7+
};
8+
9+
entrypoint!(process_instruction);
10+
fn process_instruction(
11+
program_id: &Pubkey,
12+
accounts: &[AccountInfo],
13+
instruction_data: &[u8],
14+
) -> ProgramResult {
15+
crate::processor::process_instruction(program_id, accounts, instruction_data)
16+
}

src/error.rs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use num_derive::FromPrimitive;
2+
use solana_program::program_error::ProgramError;
3+
use thiserror::Error;
4+
5+
/// Errors that may be returned by Pyth.
6+
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
7+
pub enum PythError {
8+
// 0
9+
/// Invalid account data -- either insufficient data, or incorrect magic number
10+
#[error("Failed to convert account into a Pyth account")]
11+
InvalidAccountData,
12+
/// Wrong version number
13+
#[error("Incorrect version number for Pyth account")]
14+
BadVersionNumber,
15+
/// Tried reading an account with the wrong type, e.g., tried to read
16+
/// a price account as a product account.
17+
#[error("Incorrect account type")]
18+
WrongAccountType,
19+
}
20+
21+
impl From<PythError> for ProgramError {
22+
fn from(e: PythError) -> Self {
23+
ProgramError::Custom(e as u32)
24+
}
25+
}

0 commit comments

Comments
 (0)