├── .github └── workflows │ ├── publish.yml │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── Xargo.toml ├── examples └── get_accounts.rs ├── src ├── entrypoint.rs ├── error.rs ├── instruction.rs ├── lib.rs ├── price_conf.rs └── processor.rs └── tests ├── common.rs ├── instruction_count.rs └── stale_price.rs /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to crates.io 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | jobs: 8 | publish: 9 | name: Publish 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout sources 13 | uses: actions/checkout@v2 14 | 15 | - name: Install stable toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | override: true 21 | 22 | - run: cargo publish --token ${CARGO_REGISTRY_TOKEN} 23 | env: 24 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install dependencies 19 | run: sudo apt-get install libudev-dev 20 | - name: Install Solana Binaries 21 | run: sh -c "$(curl -sSfL https://release.solana.com/v1.8.14/install)" 22 | - name: Build 23 | run: cargo build --verbose 24 | - name: Run tests 25 | run: cargo test --verbose 26 | - name: Build BPF 27 | run: PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" cargo build-bpf --verbose 28 | - name: Run BPF tests 29 | run: PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" cargo test-bpf --verbose 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | debug 3 | target 4 | Cargo.lock 5 | 6 | # IntelliJ temp files 7 | .idea 8 | *.iml -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyth-client" 3 | version = "0.5.1" 4 | authors = ["Pyth Data Foundation"] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | homepage = "https://pyth.network" 8 | repository = "https://github.com/pyth-network/pyth-client-rs" 9 | description = "pyth price oracle data structures and example usage" 10 | keywords = [ "pyth", "solana", "oracle" ] 11 | readme = "README.md" 12 | 13 | [badges] 14 | maintenance = { status = "deprecated" } 15 | 16 | [features] 17 | test-bpf = [] 18 | no-entrypoint = [] 19 | 20 | [dependencies] 21 | solana-program = "1.8.1" 22 | borsh = "0.9" 23 | borsh-derive = "0.9.0" 24 | bytemuck = "1.7.2" 25 | num-derive = "0.3" 26 | num-traits = "0.2" 27 | thiserror = "1.0" 28 | serde = { version = "1.0.136", features = ["derive"] } 29 | 30 | [dev-dependencies] 31 | solana-program-test = "1.8.1" 32 | solana-client = "1.8.1" 33 | solana-sdk = "1.8.1" 34 | 35 | [lib] 36 | crate-type = ["cdylib", "lib"] 37 | 38 | [package.metadata.docs.rs] 39 | targets = ["x86_64-unknown-linux-gnu"] 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2021] [Pyth Data Foundation] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyth Client 2 | 3 | **This crate has been deprecated. Please use [pyth-sdk-solana](https://github.com/pyth-network/pyth-sdk-rs/tree/main/pyth-sdk-solana) instead. pyth-sdk-solana provides identical functionalities with an easier interface.** 4 | 5 | This crate provides utilities for reading price feeds from the [pyth.network](https://pyth.network/) oracle on the Solana network. 6 | The crate includes a library for on-chain programs and an off-chain example program. 7 | 8 | Key features of this library include: 9 | 10 | * Get the current price of over [50 products](https://pyth.network/markets/), including cryptocurrencies, 11 | US equities, forex and more. 12 | * Combine listed products to create new price feeds, e.g., for baskets of tokens or non-USD quote currencies. 13 | * Consume prices in on-chain Solana programs or off-chain applications. 14 | 15 | Please see the [pyth.network documentation](https://docs.pyth.network/) for more information about pyth.network. 16 | 17 | ## Installation 18 | 19 | Add a dependency to your Cargo.toml: 20 | 21 | ```toml 22 | [dependencies] 23 | pyth-client="" 24 | ``` 25 | 26 | If you want to use this library in your on-chain program you should use `no-entrypoint` feature to prevent conflict between your program and this library's program. 27 | 28 | ```toml 29 | [dependencies] 30 | pyth-client = {version = "", features = ["no-entrypoint"]} 31 | ``` 32 | 33 | See [pyth-client on crates.io](https://crates.io/crates/pyth-client/) to get the latest version of the library. 34 | 35 | ## Usage 36 | 37 | Pyth Network stores its price feeds in a collection of Solana accounts. 38 | This crate provides utilities for interpreting and manipulating the content of these accounts. 39 | Applications can obtain the content of these accounts in two different ways: 40 | * On-chain programs should pass these accounts to the instructions that require price feeds. 41 | * Off-chain programs can access these accounts using the Solana RPC client (as in the [example program](examples/get_accounts.rs)). 42 | 43 | In both cases, the content of the account will be provided to the application as a binary blob (`Vec`). 44 | The examples below assume that the user has already obtained this account data. 45 | 46 | ### Parse account data 47 | 48 | Pyth Network has several different types of accounts: 49 | * Price accounts store the current price for a product 50 | * Product accounts store metadata about a product, such as its symbol (e.g., "BTC/USD"). 51 | * Mapping accounts store a listing of all Pyth accounts 52 | 53 | For more information on the different types of Pyth accounts, see the [account structure documentation](https://docs.pyth.network/how-pyth-works/account-structure). 54 | The pyth.network website also lists the public keys of the accounts (e.g., [BTC/USD accounts](https://pyth.network/markets/#BTC/USD)). 55 | 56 | This library provides several `load_*` methods that translate the binary data in each account into an appropriate struct: 57 | 58 | ```rust 59 | // replace with account data, either passed to on-chain program or from RPC node 60 | let price_account_data: Vec = ...; 61 | let price_account: Price = load_price( &price_account_data ).unwrap(); 62 | 63 | let product_account_data: Vec = ...; 64 | let product_account: Product = load_product( &product_account_data ).unwrap(); 65 | 66 | let mapping_account_data: Vec = ...; 67 | let mapping_account: Mapping = load_mapping( &mapping_account_data ).unwrap(); 68 | ``` 69 | 70 | ### Get the current price 71 | 72 | Read the current price from a `Price` account: 73 | 74 | ```rust 75 | let price: PriceConf = price_account.get_current_price().unwrap(); 76 | println!("price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo); 77 | ``` 78 | 79 | The price is returned along with a confidence interval that represents the degree of uncertainty in the price. 80 | Both values are represented as fixed-point numbers, `a * 10^e`. 81 | The method will return `None` if the price is not currently available. 82 | 83 | The status of the price feed determines if the price is available. You can get the current status using: 84 | 85 | ```rust 86 | let price_status: PriceStatus = price_account.get_current_price_status(); 87 | ``` 88 | 89 | ### Non-USD prices 90 | 91 | Most assets in Pyth are priced in USD. 92 | Applications can combine two USD prices to price an asset in a different quote currency: 93 | 94 | ```rust 95 | let btc_usd: Price = ...; 96 | let eth_usd: Price = ...; 97 | // -8 is the desired exponent for the result 98 | let btc_eth: PriceConf = btc_usd.get_price_in_quote(ð_usd, -8); 99 | println!("BTC/ETH price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo); 100 | ``` 101 | 102 | ### Price a basket of assets 103 | 104 | Applications can also compute the value of a basket of multiple assets: 105 | 106 | ```rust 107 | let btc_usd: Price = ...; 108 | let eth_usd: Price = ...; 109 | // Quantity of each asset in fixed-point a * 10^e. 110 | // This represents 0.1 BTC and .05 ETH. 111 | // -8 is desired exponent for result 112 | let basket_price: PriceConf = Price::price_basket(&[ 113 | (btc_usd, 10, -2), 114 | (eth_usd, 5, -2) 115 | ], -8); 116 | println!("0.1 BTC and 0.05 ETH are worth: ({} +- {}) x 10^{} USD", 117 | basket_price.price, basket_price.conf, basket_price.expo); 118 | ``` 119 | 120 | This function additionally propagates any uncertainty in the price into uncertainty in the value of the basket. 121 | 122 | ### Off-chain example program 123 | 124 | The example program prints the product reference data and current price information for Pyth on Solana devnet. 125 | Run the following commands to try this example program: 126 | 127 | ``` 128 | cargo build --examples 129 | cargo run --example get_accounts 130 | ``` 131 | 132 | The output of this command is a listing of Pyth's accounts, such as: 133 | 134 | ``` 135 | product_account ............ 6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8 136 | symbol.................... SRM/USD 137 | asset_type................ Crypto 138 | quote_currency............ USD 139 | description............... SRM/USD 140 | generic_symbol............ SRMUSD 141 | base...................... SRM 142 | price_account ............ 992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs 143 | price .................. 7398000000 144 | conf ................... 3200000 145 | price_type ............. price 146 | exponent ............... -9 147 | status ................. trading 148 | corp_act ............... nocorpact 149 | num_qt ................. 1 150 | valid_slot ............. 91340924 151 | publish_slot ........... 91340925 152 | ema_price .............. 7426390900 153 | ema_confidence ......... 2259870 154 | ``` 155 | 156 | ## Development 157 | 158 | This library can be built for either your native platform or in BPF (used by Solana programs). 159 | Use `cargo build` / `cargo test` to build and test natively. 160 | 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). 161 | 162 | The BPF tests will also run an instruction count program that logs the resource consumption 163 | of various library functions. 164 | This program can also be run on its own using `cargo test-bpf --test instruction_count`. 165 | 166 | ### Releases 167 | 168 | To release a new version of this package, perform the following steps: 169 | 170 | 1. Increment the version number in `Cargo.toml`. 171 | You may use a version number with a `-beta.x` suffix such as `0.0.1-beta.0` to create opt-in test versions. 172 | 2. Merge your change into `main` on github. 173 | 3. Create and publish a new github release. 174 | The name of the release should be the version number, and the tag should be the version number prefixed with `v`. 175 | 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`. 176 | -------------------------------------------------------------------------------- /Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /examples/get_accounts.rs: -------------------------------------------------------------------------------- 1 | // example usage of pyth-client account structure 2 | // bootstrap all product and pricing accounts from root mapping account 3 | 4 | use pyth_client::{ 5 | PriceType, 6 | PriceStatus, 7 | CorpAction, 8 | load_mapping, 9 | load_product, 10 | load_price 11 | }; 12 | use solana_client::{ 13 | rpc_client::RpcClient 14 | }; 15 | use solana_program::{ 16 | pubkey::Pubkey 17 | }; 18 | use std::{ 19 | str::FromStr 20 | }; 21 | 22 | fn get_price_type( ptype: &PriceType ) -> &'static str 23 | { 24 | match ptype { 25 | PriceType::Unknown => "unknown", 26 | PriceType::Price => "price", 27 | } 28 | } 29 | 30 | fn get_status( st: &PriceStatus ) -> &'static str 31 | { 32 | match st { 33 | PriceStatus::Unknown => "unknown", 34 | PriceStatus::Trading => "trading", 35 | PriceStatus::Halted => "halted", 36 | PriceStatus::Auction => "auction", 37 | } 38 | } 39 | 40 | fn get_corp_act( cact: &CorpAction ) -> &'static str 41 | { 42 | match cact { 43 | CorpAction::NoCorpAct => "nocorpact", 44 | } 45 | } 46 | 47 | fn main() { 48 | // get pyth mapping account 49 | let url = "http://api.devnet.solana.com"; 50 | let key = "BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2"; 51 | let clnt = RpcClient::new( url.to_string() ); 52 | let mut akey = Pubkey::from_str( key ).unwrap(); 53 | 54 | loop { 55 | // get Mapping account from key 56 | let map_data = clnt.get_account_data( &akey ).unwrap(); 57 | let map_acct = load_mapping( &map_data ).unwrap(); 58 | 59 | // iget and print each Product in Mapping directory 60 | let mut i = 0; 61 | for prod_akey in &map_acct.products { 62 | let prod_pkey = Pubkey::new( &prod_akey.val ); 63 | let prod_data = clnt.get_account_data( &prod_pkey ).unwrap(); 64 | let prod_acct = load_product( &prod_data ).unwrap(); 65 | 66 | // print key and reference data for this Product 67 | println!( "product_account .. {:?}", prod_pkey ); 68 | for (key, val) in prod_acct.iter() { 69 | if key.len() > 0 { 70 | println!( " {:.<16} {}", key, val ); 71 | } 72 | } 73 | 74 | // print all Prices that correspond to this Product 75 | if prod_acct.px_acc.is_valid() { 76 | let mut px_pkey = Pubkey::new( &prod_acct.px_acc.val ); 77 | loop { 78 | let pd = clnt.get_account_data( &px_pkey ).unwrap(); 79 | let pa = load_price( &pd ).unwrap(); 80 | 81 | println!( " price_account .. {:?}", px_pkey ); 82 | 83 | let maybe_price = pa.get_current_price(); 84 | match maybe_price { 85 | Some(p) => { 86 | println!(" price ........ {} x 10^{}", p.price, p.expo); 87 | println!(" conf ......... {} x 10^{}", p.conf, p.expo); 88 | } 89 | None => { 90 | println!(" price ........ unavailable"); 91 | println!(" conf ......... unavailable"); 92 | } 93 | } 94 | 95 | println!( " price_type ... {}", get_price_type(&pa.ptype)); 96 | println!( " exponent ..... {}", pa.expo ); 97 | println!( " status ....... {}", get_status(&pa.get_current_price_status())); 98 | println!( " corp_act ..... {}", get_corp_act(&pa.agg.corp_act)); 99 | 100 | println!( " num_qt ....... {}", pa.num_qt ); 101 | println!( " valid_slot ... {}", pa.valid_slot ); 102 | println!( " publish_slot . {}", pa.agg.pub_slot ); 103 | 104 | let maybe_ema_price = pa.get_ema_price(); 105 | match maybe_ema_price { 106 | Some(ema_price) => { 107 | println!( " ema_price ......... {} x 10^{}", ema_price.price, ema_price.expo ); 108 | println!( " ema_confidence ......... {} x 10^{}", ema_price.conf, ema_price.expo ); 109 | } 110 | None => { 111 | println!( " ema_price ......... unavailable"); 112 | println!( " ema_confidence ......... unavailable"); 113 | } 114 | } 115 | 116 | // go to next price account in list 117 | if pa.next.is_valid() { 118 | px_pkey = Pubkey::new( &pa.next.val ); 119 | } else { 120 | break; 121 | } 122 | } 123 | } 124 | // go to next product 125 | i += 1; 126 | if i == map_acct.num { 127 | break; 128 | } 129 | } 130 | 131 | // go to next Mapping account in list 132 | if !map_acct.next.is_valid() { 133 | break; 134 | } 135 | akey = Pubkey::new( &map_acct.next.val ); 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /src/entrypoint.rs: -------------------------------------------------------------------------------- 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 | } 17 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 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 for ProgramError { 22 | fn from(e: PythError) -> Self { 23 | ProgramError::Custom(e as u32) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/instruction.rs: -------------------------------------------------------------------------------- 1 | //! Program instructions for end-to-end testing and instruction counts 2 | 3 | use bytemuck::bytes_of; 4 | 5 | use crate::{PriceStatus, Price}; 6 | 7 | use { 8 | crate::id, 9 | borsh::{BorshDeserialize, BorshSerialize}, 10 | solana_program::instruction::Instruction, 11 | crate::PriceConf, 12 | }; 13 | 14 | /// Instructions supported by the pyth-client program, used for testing and 15 | /// instruction counts 16 | #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] 17 | pub enum PythClientInstruction { 18 | Divide { 19 | numerator: PriceConf, 20 | denominator: PriceConf, 21 | }, 22 | Multiply { 23 | x: PriceConf, 24 | y: PriceConf, 25 | }, 26 | Add { 27 | x: PriceConf, 28 | y: PriceConf, 29 | }, 30 | ScaleToExponent { 31 | x: PriceConf, 32 | expo: i32, 33 | }, 34 | Normalize { 35 | x: PriceConf, 36 | }, 37 | /// Don't do anything for comparison 38 | /// 39 | /// No accounts required for this instruction 40 | Noop, 41 | 42 | PriceStatusCheck { 43 | // A Price serialized as a vector of bytes. This field is stored as a vector of bytes (instead of a Price) 44 | // so that we do not have to add Borsh serialization to all structs, which is expensive. 45 | price_account_data: Vec, 46 | expected_price_status: PriceStatus 47 | } 48 | } 49 | 50 | pub fn divide(numerator: PriceConf, denominator: PriceConf) -> Instruction { 51 | Instruction { 52 | program_id: id(), 53 | accounts: vec![], 54 | data: PythClientInstruction::Divide { numerator, denominator } 55 | .try_to_vec() 56 | .unwrap(), 57 | } 58 | } 59 | 60 | pub fn multiply(x: PriceConf, y: PriceConf) -> Instruction { 61 | Instruction { 62 | program_id: id(), 63 | accounts: vec![], 64 | data: PythClientInstruction::Multiply { x, y } 65 | .try_to_vec() 66 | .unwrap(), 67 | } 68 | } 69 | 70 | pub fn add(x: PriceConf, y: PriceConf) -> Instruction { 71 | Instruction { 72 | program_id: id(), 73 | accounts: vec![], 74 | data: PythClientInstruction::Add { x, y } 75 | .try_to_vec() 76 | .unwrap(), 77 | } 78 | } 79 | 80 | pub fn scale_to_exponent(x: PriceConf, expo: i32) -> Instruction { 81 | Instruction { 82 | program_id: id(), 83 | accounts: vec![], 84 | data: PythClientInstruction::ScaleToExponent { x, expo } 85 | .try_to_vec() 86 | .unwrap(), 87 | } 88 | } 89 | 90 | pub fn normalize(x: PriceConf) -> Instruction { 91 | Instruction { 92 | program_id: id(), 93 | accounts: vec![], 94 | data: PythClientInstruction::Normalize { x } 95 | .try_to_vec() 96 | .unwrap(), 97 | } 98 | } 99 | 100 | /// Noop instruction for comparison purposes 101 | pub fn noop() -> Instruction { 102 | Instruction { 103 | program_id: id(), 104 | accounts: vec![], 105 | data: PythClientInstruction::Noop.try_to_vec().unwrap(), 106 | } 107 | } 108 | 109 | // Returns ok if price account status matches given expected price status. 110 | pub fn price_status_check(price: &Price, expected_price_status: PriceStatus) -> Instruction { 111 | Instruction { 112 | program_id: id(), 113 | accounts: vec![], 114 | data: PythClientInstruction::PriceStatusCheck { price_account_data: bytes_of(price).to_vec(), expected_price_status } 115 | .try_to_vec() 116 | .unwrap(), 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Rust library for consuming price feeds from the [pyth.network](https://pyth.network/) oracle on the Solana network. 2 | //! 3 | //! Please see the [crates.io page](https://crates.io/crates/pyth-client/) for documentation and example usage. 4 | #![deprecated = "This crate has been deprecated. Please use pyth-sdk-solana instead."] 5 | 6 | pub use self::price_conf::PriceConf; 7 | pub use self::error::PythError; 8 | 9 | mod entrypoint; 10 | mod error; 11 | mod price_conf; 12 | 13 | pub mod processor; 14 | pub mod instruction; 15 | 16 | use std::mem::size_of; 17 | use borsh::{BorshSerialize, BorshDeserialize}; 18 | use bytemuck::{ 19 | cast_slice, from_bytes, try_cast_slice, 20 | Pod, PodCastError, Zeroable, 21 | }; 22 | 23 | #[cfg(target_arch = "bpf")] 24 | use solana_program::{clock::Clock, sysvar::Sysvar}; 25 | 26 | solana_program::declare_id!("PythC11111111111111111111111111111111111111"); 27 | 28 | pub const MAGIC : u32 = 0xa1b2c3d4; 29 | pub const VERSION_2 : u32 = 2; 30 | pub const VERSION : u32 = VERSION_2; 31 | pub const MAP_TABLE_SIZE : usize = 640; 32 | pub const PROD_ACCT_SIZE : usize = 512; 33 | pub const PROD_HDR_SIZE : usize = 48; 34 | pub const PROD_ATTR_SIZE : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; 35 | pub const MAX_SLOT_DIFFERENCE : u64 = 25; 36 | 37 | /// The type of Pyth account determines what data it contains 38 | #[derive(Copy, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)] 39 | #[repr(C)] 40 | pub enum AccountType 41 | { 42 | Unknown, 43 | Mapping, 44 | Product, 45 | Price 46 | } 47 | 48 | impl Default for AccountType { 49 | fn default() -> Self { 50 | AccountType::Unknown 51 | } 52 | } 53 | 54 | /// The current status of a price feed. 55 | #[derive(Copy, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)] 56 | #[repr(C)] 57 | pub enum PriceStatus 58 | { 59 | /// The price feed is not currently updating for an unknown reason. 60 | Unknown, 61 | /// The price feed is updating as expected. 62 | Trading, 63 | /// The price feed is not currently updating because trading in the product has been halted. 64 | Halted, 65 | /// The price feed is not currently updating because an auction is setting the price. 66 | Auction 67 | } 68 | 69 | impl Default for PriceStatus { 70 | fn default() -> Self { 71 | PriceStatus::Unknown 72 | } 73 | } 74 | 75 | /// Status of any ongoing corporate actions. 76 | /// (still undergoing dev) 77 | #[derive(Copy, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)] 78 | #[repr(C)] 79 | pub enum CorpAction 80 | { 81 | NoCorpAct 82 | } 83 | 84 | impl Default for CorpAction { 85 | fn default() -> Self { 86 | CorpAction::NoCorpAct 87 | } 88 | } 89 | 90 | /// The type of prices associated with a product -- each product may have multiple price feeds of different types. 91 | #[derive(Copy, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)] 92 | #[repr(C)] 93 | pub enum PriceType 94 | { 95 | Unknown, 96 | Price 97 | } 98 | 99 | impl Default for PriceType { 100 | fn default() -> Self { 101 | PriceType::Unknown 102 | } 103 | } 104 | 105 | /// Public key of a Solana account 106 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)] 107 | #[repr(C)] 108 | pub struct AccKey 109 | { 110 | pub val: [u8;32] 111 | } 112 | 113 | /// Mapping accounts form a linked-list containing the listing of all products on Pyth. 114 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 115 | #[repr(C)] 116 | pub struct Mapping 117 | { 118 | /// pyth magic number 119 | pub magic : u32, 120 | /// program version 121 | pub ver : u32, 122 | /// account type 123 | pub atype : u32, 124 | /// account used size 125 | pub size : u32, 126 | /// number of product accounts 127 | pub num : u32, 128 | pub unused : u32, 129 | /// next mapping account (if any) 130 | pub next : AccKey, 131 | pub products : [AccKey;MAP_TABLE_SIZE] 132 | } 133 | 134 | #[cfg(target_endian = "little")] 135 | unsafe impl Zeroable for Mapping {} 136 | 137 | #[cfg(target_endian = "little")] 138 | unsafe impl Pod for Mapping {} 139 | 140 | 141 | /// Product accounts contain metadata for a single product, such as its symbol ("Crypto.BTC/USD") 142 | /// and its base/quote currencies. 143 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 144 | #[repr(C)] 145 | pub struct Product 146 | { 147 | /// pyth magic number 148 | pub magic : u32, 149 | /// program version 150 | pub ver : u32, 151 | /// account type 152 | pub atype : u32, 153 | /// price account size 154 | pub size : u32, 155 | /// first price account in list 156 | pub px_acc : AccKey, 157 | /// key/value pairs of reference attr. 158 | pub attr : [u8;PROD_ATTR_SIZE] 159 | } 160 | 161 | impl Product { 162 | pub fn iter(&self) -> AttributeIter { 163 | AttributeIter { attrs: &self.attr } 164 | } 165 | } 166 | 167 | #[cfg(target_endian = "little")] 168 | unsafe impl Zeroable for Product {} 169 | 170 | #[cfg(target_endian = "little")] 171 | unsafe impl Pod for Product {} 172 | 173 | /// A price and confidence at a specific slot. This struct can represent either a 174 | /// publisher's contribution or the outcome of price aggregation. 175 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)] 176 | #[repr(C)] 177 | pub struct PriceInfo 178 | { 179 | /// the current price. 180 | /// For the aggregate price use price.get_current_price() whenever possible. It has more checks to make sure price is valid. 181 | pub price : i64, 182 | /// confidence interval around the price. 183 | /// For the aggregate confidence use price.get_current_price() whenever possible. It has more checks to make sure price is valid. 184 | pub conf : u64, 185 | /// status of price (Trading is valid). 186 | /// For the aggregate status use price.get_current_status() whenever possible. 187 | /// Price data can sometimes go stale and the function handles the status in such cases. 188 | pub status : PriceStatus, 189 | /// notification of any corporate action 190 | pub corp_act : CorpAction, 191 | pub pub_slot : u64 192 | } 193 | 194 | /// The price and confidence contributed by a specific publisher. 195 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)] 196 | #[repr(C)] 197 | pub struct PriceComp 198 | { 199 | /// key of contributing publisher 200 | pub publisher : AccKey, 201 | /// the price used to compute the current aggregate price 202 | pub agg : PriceInfo, 203 | /// The publisher's latest price. This price will be incorporated into the aggregate price 204 | /// when price aggregation runs next. 205 | pub latest : PriceInfo 206 | 207 | } 208 | 209 | /// An exponentially-weighted moving average. 210 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)] 211 | #[repr(C)] 212 | pub struct Ema 213 | { 214 | /// The current value of the EMA 215 | pub val : i64, 216 | /// numerator state for next update 217 | pub numer : i64, 218 | /// denominator state for next update 219 | pub denom : i64 220 | } 221 | 222 | /// Price accounts represent a continuously-updating price feed for a product. 223 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 224 | #[repr(C)] 225 | pub struct Price 226 | { 227 | /// pyth magic number 228 | pub magic : u32, 229 | /// program version 230 | pub ver : u32, 231 | /// account type 232 | pub atype : u32, 233 | /// price account size 234 | pub size : u32, 235 | /// price or calculation type 236 | pub ptype : PriceType, 237 | /// price exponent 238 | pub expo : i32, 239 | /// number of component prices 240 | pub num : u32, 241 | /// number of quoters that make up aggregate 242 | pub num_qt : u32, 243 | /// slot of last valid (not unknown) aggregate price 244 | pub last_slot : u64, 245 | /// valid slot-time of agg. price 246 | pub valid_slot : u64, 247 | /// exponential moving average price 248 | pub ema_price : Ema, 249 | /// exponential moving average confidence interval 250 | pub ema_confidence : Ema, 251 | /// space for future derived values 252 | pub drv1 : i64, 253 | /// space for future derived values 254 | pub drv2 : i64, 255 | /// product account key 256 | pub prod : AccKey, 257 | /// next Price account in linked list 258 | pub next : AccKey, 259 | /// valid slot of previous update 260 | pub prev_slot : u64, 261 | /// aggregate price of previous update 262 | pub prev_price : i64, 263 | /// confidence interval of previous update 264 | pub prev_conf : u64, 265 | /// space for future derived values 266 | pub drv3 : i64, 267 | /// aggregate price info 268 | pub agg : PriceInfo, 269 | /// price components one per quoter 270 | pub comp : [PriceComp;32] 271 | } 272 | 273 | #[cfg(target_endian = "little")] 274 | unsafe impl Zeroable for Price {} 275 | 276 | #[cfg(target_endian = "little")] 277 | unsafe impl Pod for Price {} 278 | 279 | impl Price { 280 | /** 281 | * Get the current status of the aggregate price. 282 | * If this lib is used on-chain it will mark price status as unknown if price has not been updated for a while. 283 | */ 284 | pub fn get_current_price_status(&self) -> PriceStatus { 285 | #[cfg(target_arch = "bpf")] 286 | if matches!(self.agg.status, PriceStatus::Trading) && 287 | Clock::get().unwrap().slot - self.agg.pub_slot > MAX_SLOT_DIFFERENCE { 288 | return PriceStatus::Unknown; 289 | } 290 | self.agg.status 291 | } 292 | 293 | /** 294 | * Get the current price and confidence interval as fixed-point numbers of the form a * 10^e. 295 | * Returns a struct containing the current price, confidence interval, and the exponent for both 296 | * numbers. Returns `None` if price information is currently unavailable for any reason. 297 | */ 298 | pub fn get_current_price(&self) -> Option { 299 | if !matches!(self.get_current_price_status(), PriceStatus::Trading) { 300 | None 301 | } else { 302 | Some(PriceConf { 303 | price: self.agg.price, 304 | conf: self.agg.conf, 305 | expo: self.expo 306 | }) 307 | } 308 | } 309 | 310 | /** 311 | * Get the exponential moving average price (ema_price) and a confidence interval on the result. 312 | * Returns `None` if the ema_price is currently unavailable. 313 | * 314 | * At the moment, the confidence interval returned by this method is computed in 315 | * a somewhat questionable way, so we do not recommend using it for high-value applications. 316 | */ 317 | pub fn get_ema_price(&self) -> Option { 318 | // This method currently cannot return None, but may do so in the future. 319 | // Note that the ema_confidence is a positive number in i64, so safe to cast to u64. 320 | Some(PriceConf { price: self.ema_price.val, conf: self.ema_confidence.val as u64, expo: self.expo }) 321 | } 322 | 323 | /** 324 | * Get the current price of this account in a different quote currency. If this account 325 | * represents the price of the product X/Z, and `quote` represents the price of the product Y/Z, 326 | * this method returns the price of X/Y. Use this method to get the price of e.g., mSOL/SOL from 327 | * the mSOL/USD and SOL/USD accounts. 328 | * 329 | * `result_expo` determines the exponent of the result, i.e., the number of digits below the decimal 330 | * point. This method returns `None` if either the price or confidence are too large to be 331 | * represented with the requested exponent. 332 | */ 333 | pub fn get_price_in_quote(&self, quote: &Price, result_expo: i32) -> Option { 334 | return match (self.get_current_price(), quote.get_current_price()) { 335 | (Some(base_price_conf), Some(quote_price_conf)) => 336 | base_price_conf.div("e_price_conf)?.scale_to_exponent(result_expo), 337 | (_, _) => None, 338 | } 339 | } 340 | 341 | /** 342 | * Get the price of a basket of currencies. Each entry in `amounts` is of the form 343 | * `(price, qty, qty_expo)`, and the result is the sum of `price * qty * 10^qty_expo`. 344 | * The result is returned with exponent `result_expo`. 345 | * 346 | * An example use case for this function is to get the value of an LP token. 347 | */ 348 | pub fn price_basket(amounts: &[(Price, i64, i32)], result_expo: i32) -> Option { 349 | assert!(amounts.len() > 0); 350 | let mut res = PriceConf { price: 0, conf: 0, expo: result_expo }; 351 | for i in 0..amounts.len() { 352 | res = res.add( 353 | &amounts[i].0.get_current_price()?.cmul(amounts[i].1, amounts[i].2)?.scale_to_exponent(result_expo)? 354 | )? 355 | } 356 | Some(res) 357 | } 358 | } 359 | 360 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 361 | struct AccKeyU64 362 | { 363 | pub val: [u64;4] 364 | } 365 | 366 | #[cfg(target_endian = "little")] 367 | unsafe impl Zeroable for AccKeyU64 {} 368 | 369 | #[cfg(target_endian = "little")] 370 | unsafe impl Pod for AccKeyU64 {} 371 | 372 | impl AccKey 373 | { 374 | pub fn is_valid( &self ) -> bool { 375 | match load::( &self.val ) { 376 | Ok(k8) => k8.val[0]!=0 || k8.val[1]!=0 || k8.val[2]!=0 || k8.val[3]!=0, 377 | Err(_) => false, 378 | } 379 | } 380 | } 381 | 382 | fn load(data: &[u8]) -> Result<&T, PodCastError> { 383 | let size = size_of::(); 384 | if data.len() >= size { 385 | Ok(from_bytes(cast_slice::(try_cast_slice( 386 | &data[0..size], 387 | )?))) 388 | } else { 389 | Err(PodCastError::SizeMismatch) 390 | } 391 | } 392 | 393 | /** Get a `Mapping` account from the raw byte value of a Solana account. */ 394 | pub fn load_mapping(data: &[u8]) -> Result<&Mapping, PythError> { 395 | let pyth_mapping = load::(&data).map_err(|_| PythError::InvalidAccountData)?; 396 | 397 | if pyth_mapping.magic != MAGIC { 398 | return Err(PythError::InvalidAccountData); 399 | } 400 | if pyth_mapping.ver != VERSION_2 { 401 | return Err(PythError::BadVersionNumber); 402 | } 403 | if pyth_mapping.atype != AccountType::Mapping as u32 { 404 | return Err(PythError::WrongAccountType); 405 | } 406 | 407 | return Ok(pyth_mapping); 408 | } 409 | 410 | /** Get a `Product` account from the raw byte value of a Solana account. */ 411 | pub fn load_product(data: &[u8]) -> Result<&Product, PythError> { 412 | let pyth_product = load::(&data).map_err(|_| PythError::InvalidAccountData)?; 413 | 414 | if pyth_product.magic != MAGIC { 415 | return Err(PythError::InvalidAccountData); 416 | } 417 | if pyth_product.ver != VERSION_2 { 418 | return Err(PythError::BadVersionNumber); 419 | } 420 | if pyth_product.atype != AccountType::Product as u32 { 421 | return Err(PythError::WrongAccountType); 422 | } 423 | 424 | return Ok(pyth_product); 425 | } 426 | 427 | /** Get a `Price` account from the raw byte value of a Solana account. */ 428 | pub fn load_price(data: &[u8]) -> Result<&Price, PythError> { 429 | let pyth_price = load::(&data).map_err(|_| PythError::InvalidAccountData)?; 430 | 431 | if pyth_price.magic != MAGIC { 432 | return Err(PythError::InvalidAccountData); 433 | } 434 | if pyth_price.ver != VERSION_2 { 435 | return Err(PythError::BadVersionNumber); 436 | } 437 | if pyth_price.atype != AccountType::Price as u32 { 438 | return Err(PythError::WrongAccountType); 439 | } 440 | 441 | return Ok(pyth_price); 442 | } 443 | 444 | 445 | pub struct AttributeIter<'a> { 446 | attrs: &'a [u8], 447 | } 448 | 449 | impl<'a> Iterator for AttributeIter<'a> { 450 | type Item = (&'a str, &'a str); 451 | 452 | fn next(&mut self) -> Option { 453 | if self.attrs.is_empty() { 454 | return None; 455 | } 456 | let (key, data) = get_attr_str(self.attrs); 457 | let (val, data) = get_attr_str(data); 458 | self.attrs = data; 459 | return Some((key, val)); 460 | } 461 | } 462 | 463 | fn get_attr_str(buf: &[u8]) -> (&str, &[u8]) { 464 | if buf.is_empty() { 465 | return ("", &[]); 466 | } 467 | let len = buf[0] as usize; 468 | let str = std::str::from_utf8(&buf[1..len + 1]).expect("attr should be ascii or utf-8"); 469 | let remaining_buf = &buf[len + 1..]; 470 | (str, remaining_buf) 471 | } 472 | -------------------------------------------------------------------------------- /src/price_conf.rs: -------------------------------------------------------------------------------- 1 | use { 2 | borsh::{BorshDeserialize, BorshSerialize}, 3 | }; 4 | 5 | // Constants for working with pyth's number representation 6 | const PD_EXPO: i32 = -9; 7 | const PD_SCALE: u64 = 1_000_000_000; 8 | const MAX_PD_V_U64: u64 = (1 << 28) - 1; 9 | 10 | /** 11 | * A price with a degree of uncertainty, represented as a price +- a confidence interval. 12 | * The confidence interval roughly corresponds to the standard error of a normal distribution. 13 | * Both the price and confidence are stored in a fixed-point numeric representation, `x * 10^expo`, 14 | * where `expo` is the exponent. For example: 15 | * 16 | * ``` 17 | * use pyth_client::PriceConf; 18 | * PriceConf { price: 12345, conf: 267, expo: -2 }; // represents 123.45 +- 2.67 19 | * PriceConf { price: 123, conf: 1, expo: 2 }; // represents 12300 +- 100 20 | * ``` 21 | * 22 | * `PriceConf` supports a limited set of mathematical operations. All of these operations will 23 | * propagate any uncertainty in the arguments into the result. However, the uncertainty in the 24 | * result may overestimate the true uncertainty (by at most a factor of `sqrt(2)`) due to 25 | * computational limitations. Furthermore, all of these operations may return `None` if their 26 | * result cannot be represented within the numeric representation (e.g., the exponent is so 27 | * small that the price does not fit into an i64). Users of these methods should (1) select 28 | * their exponents to avoid this problem, and (2) handle the `None` case gracefully. 29 | */ 30 | #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)] 31 | pub struct PriceConf { 32 | pub price: i64, 33 | pub conf: u64, 34 | pub expo: i32, 35 | } 36 | 37 | impl PriceConf { 38 | /** 39 | * Divide this price by `other` while propagating the uncertainty in both prices into the result. 40 | * 41 | * This method will automatically select a reasonable exponent for the result. If both 42 | * `self` and `other` are normalized, the exponent is `self.expo + PD_EXPO - other.expo` (i.e., 43 | * the fraction has `PD_EXPO` digits of additional precision). If they are not normalized, 44 | * this method will normalize them, resulting in an unpredictable result exponent. 45 | * If the result is used in a context that requires a specific exponent, please call 46 | * `scale_to_exponent` on it. 47 | */ 48 | pub fn div(&self, other: &PriceConf) -> Option { 49 | // PriceConf is not guaranteed to store its price/confidence in normalized form. 50 | // Normalize them here to bound the range of price/conf, which is required to perform 51 | // arithmetic operations. 52 | let base = self.normalize()?; 53 | let other = other.normalize()?; 54 | 55 | if other.price == 0 { 56 | return None; 57 | } 58 | 59 | // These use at most 27 bits each 60 | let (base_price, base_sign) = PriceConf::to_unsigned(base.price); 61 | let (other_price, other_sign) = PriceConf::to_unsigned(other.price); 62 | 63 | // Compute the midprice, base in terms of other. 64 | // Uses at most 57 bits 65 | let midprice = base_price.checked_mul(PD_SCALE)?.checked_div(other_price)?; 66 | let midprice_expo = base.expo.checked_sub(other.expo)?.checked_add(PD_EXPO)?; 67 | 68 | // Compute the confidence interval. 69 | // This code uses the 1-norm instead of the 2-norm for computational reasons. 70 | // Let p +- a and q +- b be the two arguments to this method. The correct 71 | // formula is p/q * sqrt( (a/p)^2 + (b/q)^2 ). This quantity 72 | // is difficult to compute due to the sqrt and overflow/underflow considerations. 73 | // 74 | // This code instead computes p/q * (a/p + b/q) = a/q + pb/q^2 . 75 | // This quantity is at most a factor of sqrt(2) greater than the correct result, which 76 | // shouldn't matter considering that confidence intervals are typically ~0.1% of the price. 77 | 78 | // This uses 57 bits and has an exponent of PD_EXPO. 79 | let other_confidence_pct: u64 = other.conf.checked_mul(PD_SCALE)?.checked_div(other_price)?; 80 | 81 | // first term is 57 bits, second term is 57 + 58 - 29 = 86 bits. Same exponent as the midprice. 82 | // Note: the computation of the 2nd term consumes about 3k ops. We may want to optimize this. 83 | let conf = (base.conf.checked_mul(PD_SCALE)?.checked_div(other_price)? as u128).checked_add( 84 | (other_confidence_pct as u128).checked_mul(midprice as u128)?.checked_div(PD_SCALE as u128)?)?; 85 | 86 | // Note that this check only fails if an argument's confidence interval was >> its price, 87 | // in which case None is a reasonable result, as we have essentially 0 information about the price. 88 | if conf < (u64::MAX as u128) { 89 | Some(PriceConf { 90 | price: (midprice as i64).checked_mul(base_sign)?.checked_mul(other_sign)?, 91 | conf: conf as u64, 92 | expo: midprice_expo, 93 | }) 94 | } else { 95 | None 96 | } 97 | } 98 | 99 | /** 100 | * Add `other` to this, propagating uncertainty in both prices. Requires both 101 | * `PriceConf`s to have the same exponent -- use `scale_to_exponent` on the arguments 102 | * if necessary. 103 | * 104 | * TODO: could generalize this method to support different exponents. 105 | */ 106 | pub fn add(&self, other: &PriceConf) -> Option { 107 | assert_eq!(self.expo, other.expo); 108 | 109 | let price = self.price.checked_add(other.price)?; 110 | // The conf should technically be sqrt(a^2 + b^2), but that's harder to compute. 111 | let conf = self.conf.checked_add(other.conf)?; 112 | Some(PriceConf { 113 | price, 114 | conf, 115 | expo: self.expo, 116 | }) 117 | } 118 | 119 | /** Multiply this `PriceConf` by a constant `c * 10^e`. */ 120 | pub fn cmul(&self, c: i64, e: i32) -> Option { 121 | self.mul(&PriceConf { price: c, conf: 0, expo: e }) 122 | } 123 | 124 | /** Multiply this `PriceConf` by `other`, propagating any uncertainty. */ 125 | pub fn mul(&self, other: &PriceConf) -> Option { 126 | // PriceConf is not guaranteed to store its price/confidence in normalized form. 127 | // Normalize them here to bound the range of price/conf, which is required to perform 128 | // arithmetic operations. 129 | let base = self.normalize()?; 130 | let other = other.normalize()?; 131 | 132 | // These use at most 27 bits each 133 | let (base_price, base_sign) = PriceConf::to_unsigned(base.price); 134 | let (other_price, other_sign) = PriceConf::to_unsigned(other.price); 135 | 136 | // Uses at most 27*2 = 54 bits 137 | let midprice = base_price.checked_mul(other_price)?; 138 | let midprice_expo = base.expo.checked_add(other.expo)?; 139 | 140 | // Compute the confidence interval. 141 | // This code uses the 1-norm instead of the 2-norm for computational reasons. 142 | // Note that this simplifies: pq * (a/p + b/q) = qa + pb 143 | // 27*2 + 1 bits 144 | let conf = base.conf.checked_mul(other_price)?.checked_add(other.conf.checked_mul(base_price)?)?; 145 | 146 | Some(PriceConf { 147 | price: (midprice as i64).checked_mul(base_sign)?.checked_mul(other_sign)?, 148 | conf, 149 | expo: midprice_expo, 150 | }) 151 | } 152 | 153 | /** 154 | * Get a copy of this struct where the price and confidence 155 | * have been normalized to be between `MIN_PD_V_I64` and `MAX_PD_V_I64`. 156 | */ 157 | pub fn normalize(&self) -> Option { 158 | // signed division is very expensive in op count 159 | let (mut p, s) = PriceConf::to_unsigned(self.price); 160 | let mut c = self.conf; 161 | let mut e = self.expo; 162 | 163 | while p > MAX_PD_V_U64 || c > MAX_PD_V_U64 { 164 | p = p.checked_div(10)?; 165 | c = c.checked_div(10)?; 166 | e = e.checked_add(1)?; 167 | } 168 | 169 | Some(PriceConf { 170 | price: (p as i64).checked_mul(s)?, 171 | conf: c, 172 | expo: e, 173 | }) 174 | } 175 | 176 | /** 177 | * Scale this price/confidence so that its exponent is `target_expo`. Return `None` if 178 | * this number is outside the range of numbers representable in `target_expo`, which will 179 | * happen if `target_expo` is too small. 180 | * 181 | * Warning: if `target_expo` is significantly larger than the current exponent, this function 182 | * will return 0 +- 0. 183 | */ 184 | pub fn scale_to_exponent( 185 | &self, 186 | target_expo: i32, 187 | ) -> Option { 188 | let mut delta = target_expo.checked_sub(self.expo)?; 189 | if delta >= 0 { 190 | let mut p = self.price; 191 | let mut c = self.conf; 192 | // 2nd term is a short-circuit to bound op consumption 193 | while delta > 0 && (p != 0 || c != 0) { 194 | p = p.checked_div(10)?; 195 | c = c.checked_div(10)?; 196 | delta = delta.checked_sub(1)?; 197 | } 198 | 199 | Some(PriceConf { 200 | price: p, 201 | conf: c, 202 | expo: target_expo, 203 | }) 204 | } else { 205 | let mut p = self.price; 206 | let mut c = self.conf; 207 | 208 | // Either p or c == None will short-circuit to bound op consumption 209 | while delta < 0 { 210 | p = p.checked_mul(10)?; 211 | c = c.checked_mul(10)?; 212 | delta = delta.checked_add(1)?; 213 | } 214 | 215 | Some(PriceConf { 216 | price: p, 217 | conf: c, 218 | expo: target_expo, 219 | }) 220 | } 221 | } 222 | 223 | /** 224 | * Helper function to convert signed integers to unsigned and a sign bit, which simplifies 225 | * some of the computations above. 226 | */ 227 | fn to_unsigned(x: i64) -> (u64, i64) { 228 | if x == i64::MIN { 229 | // special case because i64::MIN == -i64::MIN 230 | (i64::MAX as u64 + 1, -1) 231 | } else if x < 0 { 232 | (-x as u64, -1) 233 | } else { 234 | (x as u64, 1) 235 | } 236 | } 237 | } 238 | 239 | #[cfg(test)] 240 | mod test { 241 | use crate::price_conf::{MAX_PD_V_U64, PD_EXPO, PD_SCALE, PriceConf}; 242 | 243 | const MAX_PD_V_I64: i64 = MAX_PD_V_U64 as i64; 244 | const MIN_PD_V_I64: i64 = -MAX_PD_V_I64; 245 | 246 | fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { 247 | PriceConf { 248 | price: price, 249 | conf: conf, 250 | expo: expo, 251 | } 252 | } 253 | 254 | fn pc_scaled(price: i64, conf: u64, cur_expo: i32, expo: i32) -> PriceConf { 255 | PriceConf { 256 | price: price, 257 | conf: conf, 258 | expo: cur_expo, 259 | }.scale_to_exponent(expo).unwrap() 260 | } 261 | 262 | #[test] 263 | fn test_normalize() { 264 | fn succeeds( 265 | price1: PriceConf, 266 | expected: PriceConf, 267 | ) { 268 | assert_eq!(price1.normalize().unwrap(), expected); 269 | } 270 | 271 | fn fails( 272 | price1: PriceConf, 273 | ) { 274 | assert_eq!(price1.normalize(), None); 275 | } 276 | 277 | succeeds( 278 | pc(2 * (PD_SCALE as i64), 3 * PD_SCALE, 0), 279 | pc(2 * (PD_SCALE as i64) / 100, 3 * PD_SCALE / 100, 2) 280 | ); 281 | 282 | succeeds( 283 | pc(-2 * (PD_SCALE as i64), 3 * PD_SCALE, 0), 284 | pc(-2 * (PD_SCALE as i64) / 100, 3 * PD_SCALE / 100, 2) 285 | ); 286 | 287 | // the i64 / u64 max values are a factor of 10^11 larger than MAX_PD_V 288 | let expo = -(PD_EXPO - 2); 289 | let scale_i64 = (PD_SCALE as i64) * 100; 290 | let scale_u64 = scale_i64 as u64; 291 | succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX / scale_i64, 0, expo)); 292 | succeeds(pc(i64::MIN, 1, 0), pc(i64::MIN / scale_i64, 0, expo)); 293 | succeeds(pc(1, u64::MAX, 0), pc(0, u64::MAX / scale_u64, expo)); 294 | 295 | // exponent overflows 296 | succeeds(pc(i64::MAX, 1, i32::MAX - expo), pc(i64::MAX / scale_i64, 0, i32::MAX)); 297 | fails(pc(i64::MAX, 1, i32::MAX - expo + 1)); 298 | succeeds(pc(i64::MAX, 1, i32::MIN), pc(i64::MAX / scale_i64, 0, i32::MIN + expo)); 299 | 300 | succeeds(pc(1, u64::MAX, i32::MAX - expo), pc(0, u64::MAX / scale_u64, i32::MAX)); 301 | fails(pc(1, u64::MAX, i32::MAX - expo + 1)); 302 | } 303 | 304 | #[test] 305 | fn test_scale_to_exponent() { 306 | fn succeeds( 307 | price1: PriceConf, 308 | target: i32, 309 | expected: PriceConf, 310 | ) { 311 | assert_eq!(price1.scale_to_exponent(target).unwrap(), expected); 312 | } 313 | 314 | fn fails( 315 | price1: PriceConf, 316 | target: i32, 317 | ) { 318 | assert_eq!(price1.scale_to_exponent(target), None); 319 | } 320 | 321 | succeeds(pc(1234, 1234, 0), 0, pc(1234, 1234, 0)); 322 | succeeds(pc(1234, 1234, 0), 1, pc(123, 123, 1)); 323 | succeeds(pc(1234, 1234, 0), 2, pc(12, 12, 2)); 324 | succeeds(pc(-1234, 1234, 0), 2, pc(-12, 12, 2)); 325 | succeeds(pc(1234, 1234, 0), 4, pc(0, 0, 4)); 326 | succeeds(pc(1234, 1234, 0), -1, pc(12340, 12340, -1)); 327 | succeeds(pc(1234, 1234, 0), -2, pc(123400, 123400, -2)); 328 | succeeds(pc(1234, 1234, 0), -8, pc(123400000000, 123400000000, -8)); 329 | // insufficient precision to represent the result in this exponent 330 | fails(pc(1234, 1234, 0), -20); 331 | fails(pc(1234, 0, 0), -20); 332 | fails(pc(0, 1234, 0), -20); 333 | 334 | // fails because exponent delta overflows 335 | fails(pc(1, 1, i32::MIN), i32::MAX); 336 | } 337 | 338 | #[test] 339 | fn test_div() { 340 | fn succeeds( 341 | price1: PriceConf, 342 | price2: PriceConf, 343 | expected: PriceConf, 344 | ) { 345 | assert_eq!(price1.div(&price2).unwrap(), expected); 346 | } 347 | 348 | fn fails( 349 | price1: PriceConf, 350 | price2: PriceConf, 351 | ) { 352 | let result = price1.div(&price2); 353 | assert_eq!(result, None); 354 | } 355 | 356 | succeeds(pc(1, 1, 0), pc(1, 1, 0), pc_scaled(1, 2, 0, PD_EXPO)); 357 | succeeds(pc(1, 1, -8), pc(1, 1, -8), pc_scaled(1, 2, 0, PD_EXPO)); 358 | succeeds(pc(10, 1, 0), pc(1, 1, 0), pc_scaled(10, 11, 0, PD_EXPO)); 359 | succeeds(pc(1, 1, 1), pc(1, 1, 0), pc_scaled(10, 20, 0, PD_EXPO + 1)); 360 | succeeds(pc(1, 1, 0), pc(5, 1, 0), pc_scaled(20, 24, -2, PD_EXPO)); 361 | 362 | // Negative numbers 363 | succeeds(pc(-1, 1, 0), pc(1, 1, 0), pc_scaled(-1, 2, 0, PD_EXPO)); 364 | succeeds(pc(1, 1, 0), pc(-1, 1, 0), pc_scaled(-1, 2, 0, PD_EXPO)); 365 | succeeds(pc(-1, 1, 0), pc(-1, 1, 0), pc_scaled(1, 2, 0, PD_EXPO)); 366 | 367 | // Different exponents in the two inputs 368 | succeeds(pc(100, 10, -8), pc(2, 1, -7), pc_scaled(500_000_000, 300_000_000, -8, PD_EXPO - 1)); 369 | succeeds(pc(100, 10, -4), pc(2, 1, 0), pc_scaled(500_000, 300_000, -8, PD_EXPO + -4)); 370 | 371 | // Test with end range of possible inputs where the output should not lose precision. 372 | succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); 373 | succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); 374 | succeeds(pc(1, 1, 0), 375 | pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 376 | pc((PD_SCALE as i64) / MAX_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); 377 | 378 | succeeds(pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), pc_scaled(1, 2, 0, PD_EXPO)); 379 | succeeds(pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc_scaled(MIN_PD_V_I64, 2 * MAX_PD_V_U64, 0, PD_EXPO)); 380 | succeeds(pc(1, 1, 0), 381 | pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), 382 | pc((PD_SCALE as i64) / MIN_PD_V_I64, 2 * (PD_SCALE / MAX_PD_V_U64), PD_EXPO)); 383 | 384 | succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc_scaled(1, 2 * MAX_PD_V_U64, 0, PD_EXPO)); 385 | // This fails because the confidence interval is too large to be represented in PD_EXPO 386 | fails(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0)); 387 | 388 | // Unnormalized tests below here 389 | 390 | // More realistic inputs (get BTC price in ETH) 391 | let ten_e7: i64 = 10000000; 392 | let uten_e7: u64 = 10000000; 393 | succeeds(pc(520010 * ten_e7, 310 * uten_e7, -8), 394 | pc(38591 * ten_e7, 18 * uten_e7, -8), 395 | pc(1347490347, 1431804, -8)); 396 | 397 | // Test with end range of possible inputs to identify overflow 398 | // These inputs will lose precision due to the initial normalization. 399 | // Get the rounded versions of these inputs in order to compute the expected results. 400 | let normed = pc(i64::MAX, u64::MAX, 0).normalize().unwrap(); 401 | 402 | succeeds(pc(i64::MAX, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); 403 | succeeds(pc(i64::MAX, u64::MAX, 0), 404 | pc(1, 1, 0), 405 | pc_scaled(normed.price, 3 * (normed.price as u64), normed.expo, normed.expo + PD_EXPO)); 406 | succeeds(pc(1, 1, 0), 407 | pc(i64::MAX, u64::MAX, 0), 408 | pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / (normed.price as u64)), PD_EXPO - normed.expo)); 409 | 410 | succeeds(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); 411 | succeeds(pc(i64::MAX, 1, 0), 412 | pc(1, 1, 0), 413 | pc_scaled(normed.price, normed.price as u64, normed.expo, normed.expo + PD_EXPO)); 414 | succeeds(pc(1, 1, 0), 415 | pc(i64::MAX, 1, 0), 416 | pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed.price as u64), PD_EXPO - normed.expo)); 417 | 418 | let normed = pc(i64::MIN, u64::MAX, 0).normalize().unwrap(); 419 | let normed_c = (-normed.price) as u64; 420 | 421 | succeeds(pc(i64::MIN, u64::MAX, 0), pc(i64::MIN, u64::MAX, 0), pc_scaled(1, 4, 0, PD_EXPO)); 422 | succeeds(pc(i64::MIN, u64::MAX, 0), pc(i64::MAX, u64::MAX, 0), pc_scaled(-1, 4, 0, PD_EXPO)); 423 | succeeds(pc(i64::MIN, u64::MAX, 0), 424 | pc(1, 1, 0), 425 | pc_scaled(normed.price, 3 * normed_c, normed.expo, normed.expo + PD_EXPO)); 426 | succeeds(pc(1, 1, 0), 427 | pc(i64::MIN, u64::MAX, 0), 428 | pc((PD_SCALE as i64) / normed.price, 3 * (PD_SCALE / normed_c), PD_EXPO - normed.expo)); 429 | 430 | succeeds(pc(i64::MIN, 1, 0), pc(i64::MIN, 1, 0), pc_scaled(1, 0, 0, PD_EXPO)); 431 | succeeds(pc(i64::MIN, 1, 0), 432 | pc(1, 1, 0), 433 | pc_scaled(normed.price, normed_c, normed.expo, normed.expo + PD_EXPO)); 434 | succeeds(pc(1, 1, 0), 435 | pc(i64::MIN, 1, 0), 436 | pc((PD_SCALE as i64) / normed.price, PD_SCALE / (normed_c), PD_EXPO - normed.expo)); 437 | 438 | // Price is zero pre-normalization 439 | succeeds(pc(0, 1, 0), pc(1, 1, 0), pc_scaled(0, 1, 0, PD_EXPO)); 440 | succeeds(pc(0, 1, 0), pc(100, 1, 0), pc_scaled(0, 1, -2, PD_EXPO)); 441 | fails(pc(1, 1, 0), pc(0, 1, 0)); 442 | 443 | // Normalizing the input when the confidence is >> price produces a price of 0. 444 | fails(pc(1, 1, 0), pc(1, u64::MAX, 0)); 445 | succeeds( 446 | pc(1, u64::MAX, 0), 447 | pc(1, 1, 0), 448 | pc_scaled(0, normed.conf, normed.expo, normed.expo + PD_EXPO) 449 | ); 450 | 451 | // Exponent under/overflow. 452 | succeeds(pc(1, 1, i32::MAX), pc(1, 1, 0), pc(PD_SCALE as i64, 2 * PD_SCALE, i32::MAX + PD_EXPO)); 453 | fails(pc(1, 1, i32::MAX), pc(1, 1, -1)); 454 | 455 | succeeds(pc(1, 1, i32::MIN - PD_EXPO), pc(1, 1, 0), pc(PD_SCALE as i64, 2 * PD_SCALE, i32::MIN)); 456 | succeeds(pc(1, 1, i32::MIN), pc(1, 1, PD_EXPO), pc(PD_SCALE as i64, 2 * PD_SCALE, i32::MIN)); 457 | fails(pc(1, 1, i32::MIN - PD_EXPO), pc(1, 1, 1)); 458 | } 459 | 460 | #[test] 461 | fn test_mul() { 462 | fn succeeds( 463 | price1: PriceConf, 464 | price2: PriceConf, 465 | expected: PriceConf, 466 | ) { 467 | assert_eq!(price1.mul(&price2).unwrap(), expected); 468 | } 469 | 470 | fn fails( 471 | price1: PriceConf, 472 | price2: PriceConf, 473 | ) { 474 | let result = price1.mul(&price2); 475 | assert_eq!(result, None); 476 | } 477 | 478 | succeeds(pc(1, 1, 0), pc(1, 1, 0), pc(1, 2, 0)); 479 | succeeds(pc(1, 1, -8), pc(1, 1, -8), pc(1, 2, -16)); 480 | succeeds(pc(10, 1, 0), pc(1, 1, 0), pc(10, 11, 0)); 481 | succeeds(pc(1, 1, 1), pc(1, 1, 0), pc(1, 2, 1)); 482 | succeeds(pc(1, 1, 0), pc(5, 1, 0), pc(5, 6, 0)); 483 | 484 | // Different exponents in the two inputs 485 | succeeds(pc(100, 10, -8), pc(2, 1, -7), pc(200, 120, -15)); 486 | succeeds(pc(100, 10, -4), pc(2, 1, 0), pc(200, 120, -4)); 487 | 488 | // Zero 489 | succeeds(pc(0, 10, -4), pc(2, 1, 0), pc(0, 20, -4)); 490 | succeeds(pc(2, 1, 0), pc(0, 10, -4), pc(0, 20, -4)); 491 | 492 | // Test with end range of possible inputs where the output should not lose precision. 493 | succeeds( 494 | pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 495 | pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 496 | pc(MAX_PD_V_I64 * MAX_PD_V_I64, 2 * MAX_PD_V_U64 * MAX_PD_V_U64, 0) 497 | ); 498 | succeeds(pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc(MAX_PD_V_I64, 2 * MAX_PD_V_U64, 0)); 499 | succeeds( 500 | pc(1, MAX_PD_V_U64, 0), 501 | pc(3, 1, 0), 502 | pc(3, 1 + 3 * MAX_PD_V_U64, 0) 503 | ); 504 | 505 | succeeds(pc(1, MAX_PD_V_U64, 0), pc(1, MAX_PD_V_U64, 0), pc(1, 2 * MAX_PD_V_U64, 0)); 506 | succeeds( 507 | pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 508 | pc(1, MAX_PD_V_U64, 0), 509 | pc(MAX_PD_V_I64, MAX_PD_V_U64 + MAX_PD_V_U64 * MAX_PD_V_U64, 0) 510 | ); 511 | 512 | succeeds( 513 | pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), 514 | pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), 515 | pc(MIN_PD_V_I64 * MIN_PD_V_I64, 2 * MAX_PD_V_U64 * MAX_PD_V_U64, 0) 516 | ); 517 | succeeds( 518 | pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), 519 | pc(MAX_PD_V_I64, MAX_PD_V_U64, 0), 520 | pc(MIN_PD_V_I64 * MAX_PD_V_I64, 2 * MAX_PD_V_U64 * MAX_PD_V_U64, 0) 521 | ); 522 | succeeds(pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), pc(1, 1, 0), pc(MIN_PD_V_I64, 2 * MAX_PD_V_U64, 0)); 523 | succeeds( 524 | pc(MIN_PD_V_I64, MAX_PD_V_U64, 0), 525 | pc(1, MAX_PD_V_U64, 0), 526 | pc(MIN_PD_V_I64, MAX_PD_V_U64 + MAX_PD_V_U64 * MAX_PD_V_U64, 0) 527 | ); 528 | 529 | // Unnormalized tests below here 530 | let ten_e7: i64 = 10000000; 531 | let uten_e7: u64 = 10000000; 532 | succeeds( 533 | pc(3 * (PD_SCALE as i64), 3 * PD_SCALE, PD_EXPO), 534 | pc(2 * (PD_SCALE as i64), 4 * PD_SCALE, PD_EXPO), 535 | pc(6 * ten_e7 * ten_e7, 18 * uten_e7 * uten_e7, -14) 536 | ); 537 | 538 | // Test with end range of possible inputs to identify overflow 539 | // These inputs will lose precision due to the initial normalization. 540 | // Get the rounded versions of these inputs in order to compute the expected results. 541 | let normed = pc(i64::MAX, u64::MAX, 0).normalize().unwrap(); 542 | 543 | succeeds( 544 | pc(i64::MAX, u64::MAX, 0), 545 | pc(i64::MAX, u64::MAX, 0), 546 | pc(normed.price * normed.price, 4 * ((normed.price * normed.price) as u64), normed.expo * 2) 547 | ); 548 | succeeds(pc(i64::MAX, u64::MAX, 0), 549 | pc(1, 1, 0), 550 | pc(normed.price, 3 * (normed.price as u64), normed.expo)); 551 | 552 | succeeds( 553 | pc(i64::MAX, 1, 0), 554 | pc(i64::MAX, 1, 0), 555 | pc(normed.price * normed.price, 0, normed.expo * 2) 556 | ); 557 | succeeds(pc(i64::MAX, 1, 0), 558 | pc(1, 1, 0), 559 | pc(normed.price, normed.price as u64, normed.expo)); 560 | 561 | let normed = pc(i64::MIN, u64::MAX, 0).normalize().unwrap(); 562 | let normed_c = (-normed.price) as u64; 563 | 564 | succeeds( 565 | pc(i64::MIN, u64::MAX, 0), 566 | pc(i64::MIN, u64::MAX, 0), 567 | pc(normed.price * normed.price, 4 * (normed_c * normed_c), normed.expo * 2) 568 | ); 569 | succeeds(pc(i64::MIN, u64::MAX, 0), 570 | pc(1, 1, 0), 571 | pc(normed.price, 3 * normed_c, normed.expo)); 572 | 573 | succeeds( 574 | pc(i64::MIN, 1, 0), 575 | pc(i64::MIN, 1, 0), 576 | pc(normed.price * normed.price, 0, normed.expo * 2) 577 | ); 578 | succeeds(pc(i64::MIN, 1, 0), 579 | pc(1, 1, 0), 580 | pc(normed.price, normed_c, normed.expo)); 581 | 582 | // Exponent under/overflow. 583 | succeeds(pc(1, 1, i32::MAX), pc(1, 1, 0), pc(1, 2, i32::MAX)); 584 | succeeds(pc(1, 1, i32::MAX), pc(1, 1, -1), pc(1, 2, i32::MAX - 1)); 585 | fails(pc(1, 1, i32::MAX), pc(1, 1, 1)); 586 | 587 | succeeds(pc(1, 1, i32::MIN), pc(1, 1, 0), pc(1, 2, i32::MIN)); 588 | succeeds(pc(1, 1, i32::MIN), pc(1, 1, 1), pc(1, 2, i32::MIN + 1)); 589 | fails(pc(1, 1, i32::MIN), pc(1, 1, -1)); 590 | } 591 | } 592 | -------------------------------------------------------------------------------- /src/processor.rs: -------------------------------------------------------------------------------- 1 | //! Program instruction processor for end-to-end testing and instruction counts 2 | 3 | use borsh::BorshDeserialize; 4 | use solana_program::{ 5 | account_info::AccountInfo, 6 | entrypoint::ProgramResult, 7 | pubkey::Pubkey, 8 | program_error::ProgramError 9 | }; 10 | 11 | use crate::{ 12 | instruction::PythClientInstruction, load_price, 13 | }; 14 | 15 | pub fn process_instruction( 16 | _program_id: &Pubkey, 17 | _accounts: &[AccountInfo], 18 | input: &[u8], 19 | ) -> ProgramResult { 20 | let instruction = PythClientInstruction::try_from_slice(input).unwrap(); 21 | match instruction { 22 | PythClientInstruction::Divide { numerator, denominator } => { 23 | numerator.div(&denominator); 24 | Ok(()) 25 | } 26 | PythClientInstruction::Multiply { x, y } => { 27 | x.mul(&y); 28 | Ok(()) 29 | } 30 | PythClientInstruction::Add { x, y } => { 31 | x.add(&y); 32 | Ok(()) 33 | } 34 | PythClientInstruction::Normalize { x } => { 35 | x.normalize(); 36 | Ok(()) 37 | } 38 | PythClientInstruction::ScaleToExponent { x, expo } => { 39 | x.scale_to_exponent(expo); 40 | Ok(()) 41 | } 42 | PythClientInstruction::Noop => { 43 | Ok(()) 44 | } 45 | PythClientInstruction::PriceStatusCheck { price_account_data, expected_price_status } => { 46 | let price = load_price(&price_account_data[..])?; 47 | 48 | if price.get_current_price_status() == expected_price_status { 49 | Ok(()) 50 | } else { 51 | Err(ProgramError::Custom(0)) 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | use { 2 | pyth_client::id, 3 | pyth_client::processor::process_instruction, 4 | solana_program::instruction::Instruction, 5 | solana_program_test::*, 6 | solana_sdk::{signature::Signer, transaction::Transaction, pubkey::Pubkey}, 7 | }; 8 | 9 | // Panics if running instruction fails 10 | pub async fn test_instr_exec_ok(instr: Instruction) { 11 | let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( 12 | "pyth_client", 13 | id(), 14 | processor!(process_instruction), 15 | ) 16 | .start() 17 | .await; 18 | let mut transaction = Transaction::new_with_payer( 19 | &[instr], 20 | Some(&payer.pubkey()), 21 | ); 22 | transaction.sign(&[&payer], recent_blockhash); 23 | banks_client.process_transaction(transaction).await.unwrap() 24 | } 25 | -------------------------------------------------------------------------------- /tests/instruction_count.rs: -------------------------------------------------------------------------------- 1 | use { 2 | pyth_client::{instruction, PriceConf}, 3 | solana_program_test::*, 4 | }; 5 | 6 | mod common; 7 | use common::test_instr_exec_ok; 8 | 9 | fn pc(price: i64, conf: u64, expo: i32) -> PriceConf { 10 | PriceConf { 11 | price: price, 12 | conf: conf, 13 | expo: expo, 14 | } 15 | } 16 | 17 | #[tokio::test] 18 | async fn test_noop() { 19 | test_instr_exec_ok(instruction::noop()).await; 20 | } 21 | 22 | #[tokio::test] 23 | async fn test_scale_to_exponent_down() { 24 | test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, -1000), 1000)).await 25 | } 26 | 27 | #[tokio::test] 28 | async fn test_scale_to_exponent_up() { 29 | test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, 1000), -1000)).await 30 | } 31 | 32 | #[tokio::test] 33 | async fn test_scale_to_exponent_best_case() { 34 | test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, 10), 10)).await 35 | } 36 | 37 | #[tokio::test] 38 | async fn test_normalize_max_conf() { 39 | test_instr_exec_ok(instruction::normalize(pc(1, u64::MAX, 0))).await 40 | } 41 | 42 | #[tokio::test] 43 | async fn test_normalize_max_price() { 44 | test_instr_exec_ok(instruction::normalize(pc(i64::MAX, 1, 0))).await 45 | } 46 | 47 | #[tokio::test] 48 | async fn test_normalize_min_price() { 49 | test_instr_exec_ok(instruction::normalize(pc(i64::MIN, 1, 0))).await 50 | } 51 | 52 | #[tokio::test] 53 | async fn test_normalize_best_case() { 54 | test_instr_exec_ok(instruction::normalize(pc(1, 1, 0))).await 55 | } 56 | 57 | #[tokio::test] 58 | async fn test_div_max_price() { 59 | test_instr_exec_ok(instruction::divide( 60 | pc(i64::MAX, 1, 0), 61 | pc(1, 1, 0) 62 | )).await; 63 | } 64 | 65 | #[tokio::test] 66 | async fn test_div_max_price_2() { 67 | test_instr_exec_ok(instruction::divide( 68 | pc(i64::MAX, 1, 0), 69 | pc(i64::MAX, 1, 0) 70 | )).await; 71 | } 72 | 73 | #[tokio::test] 74 | async fn test_mul_max_price() { 75 | test_instr_exec_ok(instruction::multiply( 76 | pc(i64::MAX, 1, 2), 77 | pc(123, 1, 2), 78 | )).await; 79 | } 80 | 81 | #[tokio::test] 82 | async fn test_mul_max_price_2() { 83 | test_instr_exec_ok(instruction::multiply( 84 | pc(i64::MAX, 1, 2), 85 | pc(i64::MAX, 1, 2), 86 | )).await; 87 | } 88 | -------------------------------------------------------------------------------- /tests/stale_price.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] // Only runs on bpf, where solana programs run 2 | 3 | use { 4 | pyth_client::{MAGIC, VERSION_2, instruction, PriceType, Price, AccountType, AccKey, Ema, PriceComp, PriceInfo, CorpAction, PriceStatus}, 5 | solana_program_test::*, 6 | }; 7 | 8 | 9 | mod common; 10 | use common::test_instr_exec_ok; 11 | 12 | fn price_all_zero() -> Price { 13 | let acc_key = AccKey { 14 | val: [0; 32] 15 | }; 16 | 17 | let ema = Ema { 18 | val: 0, 19 | numer: 0, 20 | denom: 0 21 | }; 22 | 23 | let price_info = PriceInfo { 24 | conf: 0, 25 | corp_act: CorpAction::NoCorpAct, 26 | price: 0, 27 | pub_slot: 0, 28 | status: PriceStatus::Unknown 29 | }; 30 | 31 | let price_comp = PriceComp { 32 | agg: price_info, 33 | latest: price_info, 34 | publisher: acc_key 35 | }; 36 | 37 | Price { 38 | magic: MAGIC, 39 | ver: VERSION_2, 40 | atype: AccountType::Price as u32, 41 | size: 0, 42 | ptype: PriceType::Price, 43 | expo: 0, 44 | num: 0, 45 | num_qt: 0, 46 | last_slot: 0, 47 | valid_slot: 0, 48 | ema_price: ema, 49 | ema_confidence: ema, 50 | drv1: 0, 51 | drv2: 0, 52 | prod: acc_key, 53 | next: acc_key, 54 | prev_slot: 0, 55 | prev_price: 0, 56 | prev_conf: 0, 57 | drv3: 0, 58 | agg: price_info, 59 | comp: [price_comp; 32] 60 | } 61 | } 62 | 63 | 64 | #[tokio::test] 65 | async fn test_price_not_stale() { 66 | let mut price = price_all_zero(); 67 | price.agg.status = PriceStatus::Trading; 68 | test_instr_exec_ok(instruction::price_status_check(&price, PriceStatus::Trading)).await; 69 | } 70 | 71 | 72 | #[tokio::test] 73 | async fn test_price_stale() { 74 | let mut price = price_all_zero(); 75 | price.agg.status = PriceStatus::Trading; 76 | // Value 100 will cause an overflow because this is bigger than Solana slot in the test suite (its ~1-5). 77 | // As the check will be 5u - 100u ~= 1e18 > MAX_SLOT_DIFFERENCE. It can only break when Solana slot in the test suite becomes 78 | // between 100 and 100+MAX_SLOT_DIFFERENCE. 79 | price.agg.pub_slot = 100; 80 | test_instr_exec_ok(instruction::price_status_check(&price, PriceStatus::Unknown)).await; 81 | } 82 | --------------------------------------------------------------------------------