├── .github └── workflows │ ├── check-formatting.yml │ ├── publish.yml │ ├── pyth-sdk-example-anchor-contract.yml │ ├── pyth-sdk-example-solana-contract.yml │ ├── pyth-sdk-solana.yml │ └── pyth-sdk.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── sol-anchor-contract │ ├── .gitignore │ ├── Anchor.toml │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── package.json │ ├── programs │ │ └── sol-anchor-contract │ │ │ ├── Cargo.toml │ │ │ ├── Xargo.toml │ │ │ └── src │ │ │ ├── error.rs │ │ │ ├── lib.rs │ │ │ └── state.rs │ ├── rust-toolchain.toml │ ├── tests │ │ └── sol-anchor-contract.ts │ └── tsconfig.json └── sol-contract │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── build │ └── README.md │ ├── rust-toolchain.toml │ ├── scripts │ ├── build.sh │ ├── deploy.sh │ ├── invoke.sh │ ├── invoke.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json │ └── src │ ├── entrypoint.rs │ ├── instruction.rs │ ├── lib.rs │ ├── processor.rs │ └── state.rs ├── pyth-sdk-solana ├── Cargo.toml ├── README.md ├── examples │ ├── eth_price.rs │ └── get_accounts.rs ├── src │ ├── error.rs │ ├── lib.rs │ └── state.rs └── test-contract │ ├── Cargo.toml │ ├── README.md │ ├── Xargo.toml │ ├── rust-toolchain │ ├── src │ ├── entrypoint.rs │ ├── instruction.rs │ ├── lib.rs │ └── processor.rs │ └── tests │ ├── common.rs │ └── instruction_count.rs ├── pyth-sdk ├── Cargo.toml ├── README.md ├── examples │ └── schema.rs ├── schema │ └── price_feed.json └── src │ ├── lib.rs │ ├── price.rs │ └── utils.rs ├── rust-toolchain.toml └── rustfmt.toml /.github/workflows/check-formatting.yml: -------------------------------------------------------------------------------- 1 | name: Check Formatting 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: nightly 18 | components: rustfmt 19 | - uses: pre-commit/action@v3.0.0 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Pyth SDK to crates.io 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | jobs: 8 | publish-pyth-sdk: 9 | name: Publish Pyth SDK 10 | if: ${{ startsWith(github.ref, 'refs/tags/pyth-sdk-v') }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v2 15 | 16 | - run: cargo publish --token ${CARGO_REGISTRY_TOKEN} 17 | env: 18 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 19 | working-directory: "pyth-sdk" 20 | publish-pyth-sdk-solana: 21 | name: Publish Pyth SDK Solana 22 | if: ${{ startsWith(github.ref, 'refs/tags/pyth-sdk-solana-v') }} 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout sources 26 | uses: actions/checkout@v2 27 | 28 | - run: cargo publish --token ${CARGO_REGISTRY_TOKEN} 29 | env: 30 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 31 | working-directory: "pyth-sdk-solana" 32 | -------------------------------------------------------------------------------- /.github/workflows/pyth-sdk-example-anchor-contract.yml: -------------------------------------------------------------------------------- 1 | name: Pyth SDK Example Solana Contract with Anchor Library 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 | defaults: 16 | run: 17 | working-directory: ./examples/sol-anchor-contract 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install dependencies 21 | run: sudo apt-get update && sudo apt-get install libudev-dev pkg-config build-essential protobuf-compiler 22 | - name: Install solana binaries 23 | run: | 24 | sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.21/install)" 25 | echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 26 | - name: Install anchor binaries 27 | run: | 28 | cargo install --git https://github.com/coral-xyz/anchor avm --tag v0.28.0 --locked --force 29 | avm install 0.28.0 30 | avm use 0.28.0 31 | - name: Build 32 | run: anchor build 33 | -------------------------------------------------------------------------------- /.github/workflows/pyth-sdk-example-solana-contract.yml: -------------------------------------------------------------------------------- 1 | name: Pyth SDK Example Solana Contract 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 | defaults: 16 | run: 17 | working-directory: ./examples/sol-contract 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install dependencies 21 | run: sudo apt-get update && sudo apt-get install libudev-dev protobuf-compiler 22 | - name: Install solana binaries 23 | run: | 24 | sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.21/install)" 25 | echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 26 | - name: Build 27 | run: scripts/build.sh 28 | -------------------------------------------------------------------------------- /.github/workflows/pyth-sdk-solana.yml: -------------------------------------------------------------------------------- 1 | name: Solana SDK 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 | defaults: 16 | run: 17 | working-directory: ./pyth-sdk-solana 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install dependencies 21 | run: sudo apt-get update && sudo apt-get install libudev-dev protobuf-compiler 22 | - name: Build 23 | run: cargo build --verbose 24 | - name: Run tests 25 | run: cargo test --verbose 26 | test-contract: 27 | runs-on: ubuntu-latest 28 | defaults: 29 | run: 30 | working-directory: ./pyth-sdk-solana/test-contract 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Install dependencies 34 | run: sudo apt-get update && sudo apt-get install libudev-dev protobuf-compiler 35 | - name: Install Solana Binaries 36 | run: | 37 | sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.21/install)" 38 | echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 39 | - name: Build 40 | run: cargo build --verbose 41 | - name: Run tests 42 | run: cargo test --verbose 43 | - name: Build SBF 44 | run: cargo build-sbf --verbose 45 | - name: Run SBF tests 46 | run: cargo test-sbf --verbose 47 | -------------------------------------------------------------------------------- /.github/workflows/pyth-sdk.yml: -------------------------------------------------------------------------------- 1 | name: Pyth SDK 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: [ pyth-sdk/** ] 7 | pull_request: 8 | branches: [ main ] 9 | paths: [ pyth-sdk/** ] 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | defaults: 18 | run: 19 | working-directory: ./pyth-sdk 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Install dependencies 23 | run: sudo apt-get update && sudo apt-get install libudev-dev 24 | - name: Build 25 | run: cargo build --verbose 26 | - name: Run tests 27 | run: cargo test --verbose 28 | schema-check: 29 | name: Check schema changes are committed 30 | runs-on: ubuntu-latest 31 | defaults: 32 | run: 33 | working-directory: ./pyth-sdk 34 | steps: 35 | - name: Checkout sources 36 | uses: actions/checkout@v2 37 | 38 | - name: Generate Schema 39 | run: cargo run --example schema 40 | 41 | - name: Schema Changes 42 | # fails if any changes not committed 43 | run: git diff --exit-code schema 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | debug 3 | target 4 | artifacts/ 5 | 6 | # IntelliJ temp files 7 | .idea 8 | *.iml 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.2.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-added-large-files 8 | - repo: local 9 | hooks: 10 | - id: cargo-fmt-nightly 11 | name: Cargo Fmt Nightly 12 | language: "rust" 13 | entry: cargo +nightly fmt 14 | pass_filenames: false 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "pyth-sdk", 5 | "pyth-sdk-solana", 6 | "pyth-sdk-solana/test-contract", 7 | "examples/sol-contract", 8 | ] 9 | 10 | [workspace.dependencies] 11 | pyth-sdk = { path = "./pyth-sdk", version = "0.8.0" } 12 | pyth-sdk-solana = { path = "./pyth-sdk-solana", version = "0.10.4" } 13 | 14 | solana-program = ">= 1.10" 15 | borsh = "0.10.3" 16 | borsh-derive = "0.10.3" 17 | serde = "1.0.136" 18 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 Network SDK 2 | 3 | The Pyth Network Rust SDK provides utilities for reading price feeds from the [pyth.network](https://pyth.network/) oracle in on- and off-chain applications. 4 | 5 | Key features of this SDK include: 6 | 7 | * Get the current price of over [50 products](https://pyth.network/markets/), including cryptocurrencies, 8 | US equities, forex and more. 9 | * Combine listed products to create new price feeds, e.g., for baskets of tokens or non-USD quote currencies. 10 | * Consume prices in Solana programs or off-chain applications. 11 | 12 | Please see the [pyth.network documentation](https://docs.pyth.network/) for more information about pyth.network. 13 | 14 | ## Usage 15 | 16 | This repository is divided into several crates focused on specific use cases: 17 | 18 | 1. [Pyth SDK](pyth-sdk) provides common data types and interfaces for that are shared across different blockchains. 19 | 2. [Pyth SDK Solana](pyth-sdk-solana) provides an interface for reading Pyth price feeds in Solana programs. 20 | This crate may also be used in off-chain applications that read prices from the Solana blockchain. 21 | 22 | Please see the documentation for the relevant crate to get started using Pyth Network. 23 | 24 | ## Development 25 | 26 | All crates in this repository can be built for either your native platform or blockchain-specific platforms. 27 | Use `cargo build` / `cargo test` to build and test natively. 28 | 29 | ### Schema Files 30 | 31 | JSON Schema files are provided to allow others to work with the various Pyth structures in languages other than Rust. These are also used within Pyth’s own repositories, for example within the pyth-sdk-js repo. Every time these structures change, new Schema’s must be generated and committed via the cargo run --example schema command. 32 | 33 | There is currently a CI check which ensures the schema files remain up-to-date. 34 | 35 | ### Creating a Release 36 | 37 | To release a new version of any of these crates, perform the following steps within the crate being released: 38 | 39 | 1. Increment the version number in `Cargo.toml`. 40 | You may use a version number with a `-beta.x` suffix such as `0.0.1-beta.0` to create opt-in test versions. 41 | 2. Merge your change into `main` on github. 42 | 3. Create and publish a new github release with the structure of `package:vx.y.z` such as `pyth-sdk-solana:v1.3.2`. It will trigger a github action 43 | and publishes the released package in [crates.io](https://crates.io) 44 | 45 | ### pre-commit hooks 46 | pre-commit is a tool that checks and fixes simple issues (formatting, ...) before each commit. You can install it by following [their website](https://pre-commit.com/). In order to enable checks for this repo run `pre-commit install` from command-line in the root of this repo. 47 | 48 | The checks are also performed in the CI to ensure the code follows consistent formatting. 49 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | test-ledger 8 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/Anchor.toml: -------------------------------------------------------------------------------- 1 | [provider] 2 | cluster = "localnet" 3 | wallet = "~/.config/solana/id.json" 4 | 5 | [programs.localnet] 6 | sol_anchor_contract = "GFPM2LncpbWiLkePLs3QjcLVPw31B2h23FwFfhig79fh" 7 | 8 | [scripts] 9 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 10 | 11 | [test] 12 | startup_wait = 20000 13 | 14 | [[test.genesis]] 15 | address = "GFPM2LncpbWiLkePLs3QjcLVPw31B2h23FwFfhig79fh" 16 | program = "./target/deploy/sol_anchor_contract.so" 17 | 18 | [test.validator] 19 | url = "https://api.devnet.solana.com" 20 | 21 | [[test.validator.clone]] 22 | address = "EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw" 23 | 24 | [[test.validator.clone]] 25 | address = "38xoQ4oeJCBrcVvca2cGk7iV1dAfrmTR1kmhSCJQ8Jto" 26 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | 6 | [profile.release] 7 | overflow-checks = true 8 | lto = "fat" 9 | codegen-units = 1 10 | [profile.release.build-override] 11 | opt-level = 3 12 | incremental = false 13 | codegen-units = 1 14 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/README.md: -------------------------------------------------------------------------------- 1 | # Pyth SDK Example Program for Solana and the Anchor Library 2 | 3 | This example implements the same functionalities as the `sol-contract` example. 4 | The difference is that this example uses the `anchor` library while the `sol-contract` example uses the lower-level solana interface. 5 | Please refer to the README of `sol-contract` for a description of the functionalities. 6 | 7 | ## Run this program 8 | We assume that you have installed `anchor`, `npm` and `yarn`. 9 | 10 | ```shell 11 | # Generate the program key 12 | > solana-keygen new -o program_address.json 13 | 14 | # Use the pubkey generated to replace the following two places 15 | # "example_sol_anchor_contract" in Anchor.toml 16 | # "declare_id!()" in programs/example-sol-anchor-contract/src/lib.rs 17 | 18 | # Enter the directory and build this example 19 | > cd examples/sol-contract-anchor 20 | > anchor build 21 | 22 | # Change the `wallet` field in Anchor.toml to your own wallet 23 | # And then deploy the example contract; An error may occur if 24 | # your wallet does not have enough funds 25 | > anchor deploy --program-keypair program_address.json --program-name example-sol-anchor-contract 26 | 27 | # Install the client dependencies and invoke this program 28 | > anchor run install 29 | > anchor run test 30 | ``` 31 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" 5 | }, 6 | "dependencies": { 7 | "@coral-xyz/anchor": "^0.27.0" 8 | }, 9 | "devDependencies": { 10 | "chai": "^4.3.7", 11 | "mocha": "^9.2.2", 12 | "ts-mocha": "^10.0.0", 13 | "@types/bn.js": "^5.1.1", 14 | "@types/chai": "^4.3.5", 15 | "@types/mocha": "^9.1.1", 16 | "typescript": "^4.9.5", 17 | "prettier": "^2.8.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/programs/sol-anchor-contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sol-anchor-contract" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | rust-version = "1.60" 6 | edition = "2021" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | name = "sol_anchor_contract" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-idl = [] 15 | no-log-ix-name = [] 16 | cpi = ["no-entrypoint"] 17 | default = [] 18 | 19 | [dependencies] 20 | anchor-lang = "0.28.0" 21 | pyth-sdk = { path = "../../../../pyth-sdk", version = "0.8.0" } 22 | solana-program = ">= 1.10, < 2.0" 23 | pyth-sdk-solana = { path = "../../../../pyth-sdk-solana", version = "0.10.2" } 24 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/programs/sol-anchor-contract/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/programs/sol-anchor-contract/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum ErrorCode { 5 | #[msg("You are not authorized to perform this action.")] 6 | Unauthorized, 7 | #[msg("The config has already been initialized.")] 8 | ReInitialize, 9 | #[msg("The config has not been initialized.")] 10 | UnInitialize, 11 | #[msg("Argument is invalid.")] 12 | InvalidArgument, 13 | #[msg("An overflow occurs.")] 14 | Overflow, 15 | #[msg("Pyth has an internal error.")] 16 | PythError, 17 | #[msg("Pyth price oracle is offline.")] 18 | PythOffline, 19 | #[msg("The loan value is higher than the collateral value.")] 20 | LoanValueTooHigh, 21 | #[msg("Program should not try to serialize a price account.")] 22 | TryToSerializePriceAccount, 23 | } 24 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/programs/sol-anchor-contract/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod state; 4 | use state::AdminConfig; 5 | use state::PriceFeed; 6 | 7 | mod error; 8 | use error::ErrorCode; 9 | 10 | declare_id!("GFPM2LncpbWiLkePLs3QjcLVPw31B2h23FwFfhig79fh"); 11 | 12 | const BASE : f64 = 10.0; 13 | 14 | #[program] 15 | pub mod sol_anchor_contract { 16 | use super::*; 17 | 18 | pub fn init(ctx: Context, config: AdminConfig) -> Result<()> { 19 | ctx.accounts.config.set_inner(config); 20 | Ok(()) 21 | } 22 | 23 | pub fn loan_to_value( 24 | ctx: Context, 25 | loan_qty: i64, 26 | collateral_qty: i64, 27 | ) -> Result<()> { 28 | msg!("Loan quantity is {}.", loan_qty); 29 | msg!("Collateral quantity is {}.", collateral_qty); 30 | 31 | let loan_feed = &ctx.accounts.pyth_loan_account; 32 | let collateral_feed = &ctx.accounts.pyth_collateral_account; 33 | // With high confidence, the maximum value of the loan is 34 | // (price + conf) * loan_qty * 10 ^ (expo). 35 | // Here is more explanation on confidence interval in Pyth: 36 | // https://docs.pyth.network/consume-data/best-practices 37 | let current_timestamp1 = Clock::get()?.unix_timestamp; 38 | let loan_price = loan_feed 39 | .get_price_no_older_than(current_timestamp1, 60) 40 | .ok_or(ErrorCode::PythOffline)?; 41 | let loan_max_price = loan_price 42 | .price 43 | .checked_add(loan_price.conf as i64) 44 | .ok_or(ErrorCode::Overflow)?; 45 | let mut loan_max_value = loan_max_price 46 | .checked_mul(loan_qty) 47 | .ok_or(ErrorCode::Overflow)?; 48 | 49 | // WARNING : f64 SHOULD NOT BE USED IN SMART CONTRACTS, IT IS USED HERE ONLY FOR LOGGING PURPOSES 50 | // lets get the maximum loan value based on computation 51 | // i.e {} * 10^({}) 52 | // loan_max_value * 10^(loan_price.expo) 53 | let exponent: i32 = loan_price.expo; 54 | let result = BASE.powi(exponent.abs()); 55 | let result = if exponent < 0 { 1.0 / result } else { result }; 56 | let result_loan_value = loan_max_value as f64 * result; 57 | 58 | msg!( 59 | "The maximum loan value is {} * 10^({}) = {}.", 60 | loan_max_value, 61 | loan_price.expo, 62 | result_loan_value 63 | ); 64 | 65 | // With high confidence, the minimum value of the collateral is 66 | // (price - conf) * collateral_qty * 10 ^ (expo). 67 | // Here is more explanation on confidence interval in Pyth: 68 | // https://docs.pyth.network/consume-data/best-practices 69 | let current_timestamp2 = Clock::get()?.unix_timestamp; 70 | let collateral_price = collateral_feed 71 | .get_price_no_older_than(current_timestamp2, 60) 72 | .ok_or(ErrorCode::PythOffline)?; 73 | let collateral_min_price = collateral_price 74 | .price 75 | .checked_sub(collateral_price.conf as i64) 76 | .ok_or(ErrorCode::Overflow)?; 77 | let mut collateral_min_value = collateral_min_price 78 | .checked_mul(collateral_qty) 79 | .ok_or(ErrorCode::Overflow)?; 80 | 81 | // WARNING : f64 SHOULD NOT BE USED IN SMART CONTRACTS, IT IS USED HERE ONLY FOR LOGGING PURPOSES 82 | // lets get the minimum collateral value based on computation 83 | // i.e {} * 10^({}) 84 | // i.e collateral_min_value * 10^(collateral_price.expo) 85 | let exponent: i32 = collateral_price.expo; 86 | let result = BASE.powi(exponent.abs()); 87 | let result: f64 = if exponent < 0 { 1.0 / result } else { result }; 88 | let result_collateral_value = collateral_min_value as f64 * result; 89 | 90 | msg!( 91 | "The minimum collateral value is {} * 10^({}) = {}.", 92 | collateral_min_value, 93 | collateral_price.expo, 94 | result_collateral_value 95 | ); 96 | 97 | // If the loan and collateral prices use different exponent, 98 | // normalize the value. 99 | if loan_price.expo > collateral_price.expo { 100 | let normalize = (10 as i64) 101 | .checked_pow((loan_price.expo - collateral_price.expo) as u32) 102 | .ok_or(ErrorCode::Overflow)?; 103 | collateral_min_value = collateral_min_value 104 | .checked_mul(normalize) 105 | .ok_or(ErrorCode::Overflow)?; 106 | } else if loan_price.expo < collateral_price.expo { 107 | let normalize = (10 as i64) 108 | .checked_pow((collateral_price.expo - loan_price.expo) as u32) 109 | .ok_or(ErrorCode::Overflow)?; 110 | loan_max_value = loan_max_value 111 | .checked_mul(normalize) 112 | .ok_or(ErrorCode::Overflow)?; 113 | } 114 | 115 | // Check whether the value of the collateral is higher. 116 | if collateral_min_value > loan_max_value { 117 | msg!("The value of the collateral is higher."); 118 | return Ok(()); 119 | } else { 120 | return Err(error!(ErrorCode::LoanValueTooHigh)); 121 | } 122 | } 123 | } 124 | 125 | #[derive(Accounts)] 126 | pub struct InitRequest<'info> { 127 | #[account(mut)] 128 | pub payer: Signer<'info>, 129 | #[account( 130 | init, 131 | payer = payer, 132 | space = 8 + AdminConfig::INIT_SPACE 133 | )] 134 | pub config: Account<'info, AdminConfig>, 135 | pub system_program: Program<'info, System>, 136 | } 137 | 138 | #[derive(Accounts)] 139 | pub struct QueryRequest<'info> { 140 | pub config: Account<'info, AdminConfig>, 141 | #[account( 142 | address = config.loan_price_feed_id @ ErrorCode::InvalidArgument 143 | )] 144 | pub pyth_loan_account: Account<'info, PriceFeed>, 145 | #[account( 146 | address = config.collateral_price_feed_id @ ErrorCode::InvalidArgument 147 | )] 148 | pub pyth_collateral_account: Account<'info, PriceFeed>, 149 | } 150 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/programs/sol-anchor-contract/src/state.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use pyth_sdk_solana::state::load_price_account; 3 | use pyth_sdk_solana::state::SolanaPriceAccount; 4 | use std::ops::Deref; 5 | use std::str::FromStr; 6 | 7 | use crate::ErrorCode; 8 | 9 | #[account] 10 | #[derive(InitSpace)] 11 | pub struct AdminConfig { 12 | pub loan_price_feed_id: Pubkey, 13 | pub collateral_price_feed_id: Pubkey, 14 | } 15 | 16 | #[derive(Clone)] 17 | pub struct PriceFeed(pyth_sdk::PriceFeed); 18 | 19 | impl anchor_lang::Owner for PriceFeed { 20 | fn owner() -> Pubkey { 21 | // Make sure the owner is the pyth oracle account on solana devnet 22 | let oracle_addr = "gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s"; 23 | return Pubkey::from_str(&oracle_addr).unwrap(); 24 | } 25 | } 26 | 27 | impl anchor_lang::AccountDeserialize for PriceFeed { 28 | fn try_deserialize_unchecked(data: &mut &[u8]) -> Result { 29 | let account: &SolanaPriceAccount = 30 | load_price_account(data).map_err(|_x| error!(ErrorCode::PythError))?; 31 | 32 | // Use a dummy key since the key field will be removed from the SDK 33 | let zeros: [u8; 32] = [0; 32]; 34 | let dummy_key = Pubkey::new(&zeros); 35 | let feed = account.to_price_feed(&dummy_key); 36 | return Ok(PriceFeed(feed)); 37 | } 38 | } 39 | 40 | impl anchor_lang::AccountSerialize for PriceFeed { 41 | fn try_serialize(&self, _writer: &mut W) -> std::result::Result<(), Error> { 42 | Err(error!(ErrorCode::TryToSerializePriceAccount)) 43 | } 44 | } 45 | 46 | impl Deref for PriceFeed { 47 | type Target = pyth_sdk::PriceFeed; 48 | 49 | fn deref(&self) -> &Self::Target { 50 | &self.0 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.75.0" 3 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/tests/sol-anchor-contract.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { SolAnchorContract } from "../target/types/sol_anchor_contract"; 3 | 4 | describe("sol-anchor-contract", () => { 5 | 6 | const ethToUSD = "EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw"; 7 | const usdtToUSD = "38xoQ4oeJCBrcVvca2cGk7iV1dAfrmTR1kmhSCJQ8Jto"; 8 | 9 | const provider = anchor.AnchorProvider.local(); 10 | 11 | // Configure the client to use the local cluster. 12 | anchor.setProvider(provider); 13 | 14 | const config = anchor.web3.Keypair.generate(); 15 | const program = anchor.workspace 16 | .SolAnchorContract as anchor.Program; 17 | const payer = provider.wallet.publicKey; 18 | 19 | it("Initialize the config.", async () => { 20 | const tx = await program.methods 21 | .init({ 22 | loanPriceFeedId: new anchor.web3.PublicKey(ethToUSD), 23 | collateralPriceFeedId: new anchor.web3.PublicKey(usdtToUSD), 24 | }) 25 | .accounts({ 26 | payer, 27 | config: config.publicKey, 28 | systemProgram: anchor.web3.SystemProgram.programId, 29 | }) 30 | .signers([config]) 31 | .rpc(); 32 | 33 | console.log("Config key: " + config.publicKey); 34 | console.log("Init() is invoked: " + tx); 35 | }); 36 | 37 | it("Check loan to value ratio.", async () => { 38 | const tx = await program.methods 39 | .loanToValue(new anchor.BN(1), new anchor.BN(3000)) 40 | .accounts({ 41 | config: config.publicKey, 42 | pythLoanAccount: new anchor.web3.PublicKey(ethToUSD), 43 | pythCollateralAccount: new anchor.web3.PublicKey(usdtToUSD), 44 | }) 45 | .signers([]) 46 | .rpc(); 47 | 48 | console.log("Loan2Value() is invoked: " + tx); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /examples/sol-anchor-contract/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "mocha", 5 | "chai" 6 | ], 7 | "typeRoots": [ 8 | "./node_modules/@types" 9 | ], 10 | "lib": [ 11 | "es2015" 12 | ], 13 | "module": "commonjs", 14 | "target": "es6", 15 | "esModuleInterop": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/sol-contract/.gitignore: -------------------------------------------------------------------------------- 1 | build/example_sol_contract.so 2 | build/example_sol_contract-keypair.json 3 | scripts/invoke.js 4 | scripts/invoke.js.map 5 | scripts/node_modules/ 6 | -------------------------------------------------------------------------------- /examples/sol-contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-sol-contract" 3 | version = "0.2.0" 4 | authors = ["Pyth Data Foundation"] 5 | workspace = "../../" 6 | edition = "2021" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | 11 | [dependencies] 12 | arrayref = "0.3.6" 13 | borsh.workspace = true 14 | solana-program.workspace = true 15 | pyth-sdk-solana.workspace = true 16 | -------------------------------------------------------------------------------- /examples/sol-contract/README.md: -------------------------------------------------------------------------------- 1 | # Pyth SDK Example Program for Solana 2 | 3 | This is an example demonstrating how to read prices from Pyth on Solana. 4 | 5 | The program has two instructions: `Init` and `Loan2Value`. 6 | `Init` can *only* be invoked by the program admin and it will initialize some loan information. 7 | `Loan2Value` can be invoked by anyone and it uses the current Pyth price to compare the value of the loan and the value of the collateral. 8 | This is an important functionality in many lending protocols. 9 | 10 | The key program logic is in 3 files. 11 | The loan information structure is defined in `src/state.rs`, which also contains the serialization and deserialization code. 12 | The two instructions are implemented in `src/processor.rs`. 13 | An example invocation of these instructions on the Solana devnet can be found in `scripts/invoke.ts`. 14 | 15 | ## Where and how is the Pyth SDK used? 16 | Pyth SDK is used in the `Loan2Value` instruction in `src/processor.rs`. 17 | For the loan, the code first reads the unit price from the Pyth oracle. 18 | ```rust 19 | let feed1 = load_price_feed_from_account_info(pyth_loan_account)?; 20 | let current_timestamp1 = Clock::get()?.unix_timestamp; 21 | let result1 = feed1.get_price_no_older_than(current_timestamp1, 60).ok_or(ProgramError::Custom(3))?; 22 | ``` 23 | 24 | And then calculate the loan value given the quantity of the loan. 25 | ```rust 26 | let loan_max_price = result1 27 | .price 28 | .checked_add(result1.conf as i64) 29 | .ok_or(ProgramError::Custom(4))?; 30 | let loan_max_value = loan_max_price 31 | .checked_mul(loan_qty) 32 | .ok_or(ProgramError::Custom(4))?; 33 | ``` 34 | 35 | This code says that, with high confidence, the maximum value of the loan does not exceed `loan_max_value * 10^(result1.expo)` at the time of the query. 36 | In a similar way, the code then calculates the minimum value of the collateral and compare the two. 37 | 38 | More on Pyth best practice and price confidence interval can be found [here](https://docs.pyth.network/consume-data/best-practices). 39 | 40 | ## Run this program 41 | We assume that you have installed `cargo`, `solana`, `npm` and `node`. 42 | 43 | ```shell 44 | # Enter the root directory of this example 45 | > cd examples/sol-contract 46 | # Build the example contract 47 | > scripts/build.sh 48 | # Config solana CLI and set the url as devnet 49 | > solana config set --url https://api.devnet.solana.com 50 | # Deploy the example contract 51 | > scripts/deploy.sh 52 | # Invoke the example contract 53 | > scripts/invoke.sh 54 | ``` 55 | -------------------------------------------------------------------------------- /examples/sol-contract/build/README.md: -------------------------------------------------------------------------------- 1 | This directory holds the output of build-bpf. See scripts/build.sh. 2 | -------------------------------------------------------------------------------- /examples/sol-contract/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.75.0" 3 | -------------------------------------------------------------------------------- /examples/sol-contract/scripts/build.sh: -------------------------------------------------------------------------------- 1 | cargo build-bpf --bpf-out-dir ./build 2 | -------------------------------------------------------------------------------- /examples/sol-contract/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | echo "Airdropping..." 2 | solana airdrop 1 --url https://api.devnet.solana.com 3 | echo "Deploying the program..." 4 | solana program deploy --program-id build/example_sol_contract-keypair.json build/example_sol_contract.so 5 | -------------------------------------------------------------------------------- /examples/sol-contract/scripts/invoke.sh: -------------------------------------------------------------------------------- 1 | cd scripts; npm install typescript; npm run build; node invoke.js 2 | -------------------------------------------------------------------------------- /examples/sol-contract/scripts/invoke.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const web3 = require("@solana/web3.js"); 3 | const {struct, b, u8, blob} = require("@solana/buffer-layout"); 4 | 5 | export const invoke = async (loan: string, collateral: string) => { 6 | /* Obtain the contract keypair */ 7 | var contract; 8 | try { 9 | let data = fs.readFileSync( 10 | '../build/example_sol_contract-keypair.json' 11 | ); 12 | contract = web3.Keypair.fromSecretKey( 13 | new Uint8Array(JSON.parse(data)) 14 | ); 15 | console.info("Invoking contract " + contract.publicKey); 16 | } catch (error) { 17 | console.error("Please run scripts/build.sh first."); 18 | return; 19 | } 20 | 21 | /* Prepare the payer account */ 22 | let conn = new web3.Connection(web3.clusterApiUrl('devnet')); 23 | console.info("Airdropping to the payer account..."); 24 | let payer = web3.Keypair.generate(); 25 | let airdropSig = await conn.requestAirdrop( 26 | payer.publicKey, web3.LAMPORTS_PER_SOL 27 | ); 28 | await conn.confirmTransaction(airdropSig); 29 | 30 | /* Prepare the createInst instruction which creates an 31 | * account storing the AdminConfig data for the instructions */ 32 | let loanInfoSize = 1 + 32 + 32; 33 | let dataAccount = web3.Keypair.generate(); 34 | let dataCost = await conn.getMinimumBalanceForRentExemption(loanInfoSize); 35 | const createInst = web3.SystemProgram.createAccount({ 36 | lamports: dataCost, 37 | space: loanInfoSize, 38 | programId: contract.publicKey, 39 | fromPubkey: payer.publicKey, 40 | newAccountPubkey: dataAccount.publicKey, 41 | }); 42 | 43 | /* Prepare the accounts and instruction data for transactions */ 44 | const dataKey = dataAccount.publicKey; 45 | const loanKey = new web3.PublicKey(loan); 46 | const collateralKey = new web3.PublicKey(collateral); 47 | let accounts = 48 | [{pubkey: contract.publicKey, isSigner: true, isWritable: false}, 49 | {pubkey: dataKey, isSigner: false, isWritable: false}, 50 | {pubkey: loanKey, isSigner: false, isWritable: false}, 51 | {pubkey: collateralKey, isSigner: false, isWritable: false}, 52 | ]; 53 | 54 | let initLayout = struct([ u8('instruction') ]) 55 | let initData = Buffer.alloc(initLayout.span); 56 | let loan2ValueLayout = struct([ 57 | u8('instruction'), blob(8, 'loan_qty'), blob(8, 'collateral_qty') 58 | ]) 59 | let loan2ValueData = Buffer.alloc(loan2ValueLayout.span); 60 | 61 | /* Invoke the Init instruction (instruction #0) */ 62 | console.log("Creating data account and invoking Init..."); 63 | initLayout.encode({instruction: 0}, initData); 64 | let txInit = new web3.Transaction({ feePayer: payer.publicKey }); 65 | txInit.add( 66 | createInst, /* Create data account */ 67 | new web3.TransactionInstruction({ /* Initialize data account */ 68 | data: initData, 69 | keys: accounts, 70 | programId: contract.publicKey 71 | }) 72 | ); 73 | let txInitSig = await web3.sendAndConfirmTransaction( 74 | conn, txInit, [payer, dataAccount, contract] 75 | ); 76 | console.log("TxHash: " + txInitSig); 77 | 78 | /* Invoke the Loan2Value instruction (instruction #1) */ 79 | console.log("Checking loan to value ratio..."); 80 | /* Encode 0x1 in big ending */ 81 | let loan_qty = Buffer.from('0100000000000000', 'hex'); 82 | /* Encode 0xbb8 (3000) in big ending */ 83 | let collateral_qty = Buffer.from('b80b000000000000', 'hex'); 84 | loan2ValueLayout.encode( 85 | {instruction: 1, 86 | loan_qty: blob(8).decode(loan_qty), 87 | collateral_qty: blob(8).decode(collateral_qty)} 88 | , loan2ValueData); 89 | 90 | let txCheck = new web3.Transaction({ feePayer: payer.publicKey }); 91 | txCheck.add( 92 | new web3.TransactionInstruction({ 93 | data: loan2ValueData, 94 | keys: accounts, 95 | programId: contract.publicKey 96 | }) 97 | ); 98 | let txCheckSig = await web3.sendAndConfirmTransaction( 99 | conn, txCheck, [payer, contract] 100 | ); 101 | console.log("TxHash: " + txCheckSig); 102 | 103 | /* Try to invoke the Init instruction without authority */ 104 | console.log("Trying an unauthorized invocation of Init..."); 105 | let attacker = web3.Keypair.generate(); 106 | accounts[0].pubkey = attacker.publicKey 107 | 108 | let attackerDataAccount = web3.Keypair.generate(); 109 | const attackerCreateInst = web3.SystemProgram.createAccount({ 110 | lamports: dataCost, 111 | space: loanInfoSize, 112 | programId: contract.publicKey, 113 | fromPubkey: payer.publicKey, 114 | newAccountPubkey: attackerDataAccount.publicKey, 115 | }); 116 | 117 | let txAttacker = new web3.Transaction({ feePayer: payer.publicKey }); 118 | txAttacker.add( 119 | attackerCreateInst, 120 | new web3.TransactionInstruction({ 121 | data: initData, 122 | keys: accounts, 123 | programId: contract.publicKey 124 | }) 125 | ); 126 | 127 | var attacker_succeed = false, txAttackerSig; 128 | try { 129 | txAttackerSig = await web3.sendAndConfirmTransaction( 130 | conn, txAttacker, [payer, attackerDataAccount, attacker] 131 | ); 132 | attacker_succeed = true; 133 | } catch (error) { 134 | console.log("Attacker failed to invoke unauthorized Init."); 135 | } 136 | 137 | if (attacker_succeed) 138 | throw new Error("Attacker succeeded! TxHash: " + txAttackerSig); 139 | } 140 | 141 | /* Pyth price accounts on the solana devnet */ 142 | let ethToUSD = "EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw"; 143 | let usdtToUSD = "38xoQ4oeJCBrcVvca2cGk7iV1dAfrmTR1kmhSCJQ8Jto"; 144 | invoke(ethToUSD, usdtToUSD); 145 | -------------------------------------------------------------------------------- /examples/sol-contract/scripts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "scripts", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@solana/web3.js": "^1.56.2" 13 | }, 14 | "devDependencies": { 15 | "typescript": "^4.8.4" 16 | } 17 | }, 18 | "node_modules/@babel/runtime": { 19 | "version": "7.19.0", 20 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", 21 | "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", 22 | "dependencies": { 23 | "regenerator-runtime": "^0.13.4" 24 | }, 25 | "engines": { 26 | "node": ">=6.9.0" 27 | } 28 | }, 29 | "node_modules/@noble/ed25519": { 30 | "version": "1.7.1", 31 | "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.1.tgz", 32 | "integrity": "sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==", 33 | "funding": [ 34 | { 35 | "type": "individual", 36 | "url": "https://paulmillr.com/funding/" 37 | } 38 | ] 39 | }, 40 | "node_modules/@noble/hashes": { 41 | "version": "1.1.3", 42 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz", 43 | "integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==", 44 | "funding": [ 45 | { 46 | "type": "individual", 47 | "url": "https://paulmillr.com/funding/" 48 | } 49 | ] 50 | }, 51 | "node_modules/@noble/secp256k1": { 52 | "version": "1.7.0", 53 | "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.0.tgz", 54 | "integrity": "sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw==", 55 | "funding": [ 56 | { 57 | "type": "individual", 58 | "url": "https://paulmillr.com/funding/" 59 | } 60 | ] 61 | }, 62 | "node_modules/@solana/buffer-layout": { 63 | "version": "4.0.0", 64 | "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz", 65 | "integrity": "sha512-lR0EMP2HC3+Mxwd4YcnZb0smnaDw7Bl2IQWZiTevRH5ZZBZn6VRWn3/92E3qdU4SSImJkA6IDHawOHAnx/qUvQ==", 66 | "dependencies": { 67 | "buffer": "~6.0.3" 68 | }, 69 | "engines": { 70 | "node": ">=5.10" 71 | } 72 | }, 73 | "node_modules/@solana/buffer-layout/node_modules/buffer": { 74 | "version": "6.0.3", 75 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 76 | "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 77 | "funding": [ 78 | { 79 | "type": "github", 80 | "url": "https://github.com/sponsors/feross" 81 | }, 82 | { 83 | "type": "patreon", 84 | "url": "https://www.patreon.com/feross" 85 | }, 86 | { 87 | "type": "consulting", 88 | "url": "https://feross.org/support" 89 | } 90 | ], 91 | "dependencies": { 92 | "base64-js": "^1.3.1", 93 | "ieee754": "^1.2.1" 94 | } 95 | }, 96 | "node_modules/@solana/web3.js": { 97 | "version": "1.63.1", 98 | "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.63.1.tgz", 99 | "integrity": "sha512-wgEdGVK5FTS2zENxbcGSvKpGZ0jDS6BUdGu8Gn6ns0CzgJkK83u4ip3THSnBPEQ5i/jrqukg998BwV1H67+qiQ==", 100 | "dependencies": { 101 | "@babel/runtime": "^7.12.5", 102 | "@noble/ed25519": "^1.7.0", 103 | "@noble/hashes": "^1.1.2", 104 | "@noble/secp256k1": "^1.6.3", 105 | "@solana/buffer-layout": "^4.0.0", 106 | "bigint-buffer": "^1.1.5", 107 | "bn.js": "^5.0.0", 108 | "borsh": "^0.7.0", 109 | "bs58": "^4.0.1", 110 | "buffer": "6.0.1", 111 | "fast-stable-stringify": "^1.0.0", 112 | "jayson": "^3.4.4", 113 | "node-fetch": "2", 114 | "rpc-websockets": "^7.5.0", 115 | "superstruct": "^0.14.2" 116 | }, 117 | "engines": { 118 | "node": ">=12.20.0" 119 | } 120 | }, 121 | "node_modules/@types/connect": { 122 | "version": "3.4.35", 123 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", 124 | "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", 125 | "dependencies": { 126 | "@types/node": "*" 127 | } 128 | }, 129 | "node_modules/@types/node": { 130 | "version": "12.20.55", 131 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", 132 | "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" 133 | }, 134 | "node_modules/@types/ws": { 135 | "version": "7.4.7", 136 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", 137 | "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", 138 | "dependencies": { 139 | "@types/node": "*" 140 | } 141 | }, 142 | "node_modules/base-x": { 143 | "version": "3.0.9", 144 | "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", 145 | "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", 146 | "dependencies": { 147 | "safe-buffer": "^5.0.1" 148 | } 149 | }, 150 | "node_modules/base64-js": { 151 | "version": "1.5.1", 152 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 153 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 154 | "funding": [ 155 | { 156 | "type": "github", 157 | "url": "https://github.com/sponsors/feross" 158 | }, 159 | { 160 | "type": "patreon", 161 | "url": "https://www.patreon.com/feross" 162 | }, 163 | { 164 | "type": "consulting", 165 | "url": "https://feross.org/support" 166 | } 167 | ] 168 | }, 169 | "node_modules/bigint-buffer": { 170 | "version": "1.1.5", 171 | "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", 172 | "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", 173 | "hasInstallScript": true, 174 | "dependencies": { 175 | "bindings": "^1.3.0" 176 | }, 177 | "engines": { 178 | "node": ">= 10.0.0" 179 | } 180 | }, 181 | "node_modules/bindings": { 182 | "version": "1.5.0", 183 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 184 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 185 | "dependencies": { 186 | "file-uri-to-path": "1.0.0" 187 | } 188 | }, 189 | "node_modules/bn.js": { 190 | "version": "5.2.1", 191 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", 192 | "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" 193 | }, 194 | "node_modules/borsh": { 195 | "version": "0.7.0", 196 | "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", 197 | "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", 198 | "dependencies": { 199 | "bn.js": "^5.2.0", 200 | "bs58": "^4.0.0", 201 | "text-encoding-utf-8": "^1.0.2" 202 | } 203 | }, 204 | "node_modules/bs58": { 205 | "version": "4.0.1", 206 | "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", 207 | "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", 208 | "dependencies": { 209 | "base-x": "^3.0.2" 210 | } 211 | }, 212 | "node_modules/buffer": { 213 | "version": "6.0.1", 214 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.1.tgz", 215 | "integrity": "sha512-rVAXBwEcEoYtxnHSO5iWyhzV/O1WMtkUYWlfdLS7FjU4PnSJJHEfHXi/uHPI5EwltmOA794gN3bm3/pzuctWjQ==", 216 | "funding": [ 217 | { 218 | "type": "github", 219 | "url": "https://github.com/sponsors/feross" 220 | }, 221 | { 222 | "type": "patreon", 223 | "url": "https://www.patreon.com/feross" 224 | }, 225 | { 226 | "type": "consulting", 227 | "url": "https://feross.org/support" 228 | } 229 | ], 230 | "dependencies": { 231 | "base64-js": "^1.3.1", 232 | "ieee754": "^1.2.1" 233 | } 234 | }, 235 | "node_modules/bufferutil": { 236 | "version": "4.0.6", 237 | "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz", 238 | "integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==", 239 | "hasInstallScript": true, 240 | "optional": true, 241 | "dependencies": { 242 | "node-gyp-build": "^4.3.0" 243 | }, 244 | "engines": { 245 | "node": ">=6.14.2" 246 | } 247 | }, 248 | "node_modules/commander": { 249 | "version": "2.20.3", 250 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 251 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 252 | }, 253 | "node_modules/delay": { 254 | "version": "5.0.0", 255 | "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", 256 | "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", 257 | "engines": { 258 | "node": ">=10" 259 | }, 260 | "funding": { 261 | "url": "https://github.com/sponsors/sindresorhus" 262 | } 263 | }, 264 | "node_modules/es6-promise": { 265 | "version": "4.2.8", 266 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 267 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" 268 | }, 269 | "node_modules/es6-promisify": { 270 | "version": "5.0.0", 271 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 272 | "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", 273 | "dependencies": { 274 | "es6-promise": "^4.0.3" 275 | } 276 | }, 277 | "node_modules/eventemitter3": { 278 | "version": "4.0.7", 279 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", 280 | "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" 281 | }, 282 | "node_modules/eyes": { 283 | "version": "0.1.8", 284 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 285 | "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", 286 | "engines": { 287 | "node": "> 0.1.90" 288 | } 289 | }, 290 | "node_modules/fast-stable-stringify": { 291 | "version": "1.0.0", 292 | "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", 293 | "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" 294 | }, 295 | "node_modules/file-uri-to-path": { 296 | "version": "1.0.0", 297 | "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 298 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" 299 | }, 300 | "node_modules/ieee754": { 301 | "version": "1.2.1", 302 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 303 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 304 | "funding": [ 305 | { 306 | "type": "github", 307 | "url": "https://github.com/sponsors/feross" 308 | }, 309 | { 310 | "type": "patreon", 311 | "url": "https://www.patreon.com/feross" 312 | }, 313 | { 314 | "type": "consulting", 315 | "url": "https://feross.org/support" 316 | } 317 | ] 318 | }, 319 | "node_modules/isomorphic-ws": { 320 | "version": "4.0.1", 321 | "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", 322 | "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", 323 | "peerDependencies": { 324 | "ws": "*" 325 | } 326 | }, 327 | "node_modules/jayson": { 328 | "version": "3.7.0", 329 | "resolved": "https://registry.npmjs.org/jayson/-/jayson-3.7.0.tgz", 330 | "integrity": "sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==", 331 | "dependencies": { 332 | "@types/connect": "^3.4.33", 333 | "@types/node": "^12.12.54", 334 | "@types/ws": "^7.4.4", 335 | "commander": "^2.20.3", 336 | "delay": "^5.0.0", 337 | "es6-promisify": "^5.0.0", 338 | "eyes": "^0.1.8", 339 | "isomorphic-ws": "^4.0.1", 340 | "json-stringify-safe": "^5.0.1", 341 | "JSONStream": "^1.3.5", 342 | "lodash": "^4.17.20", 343 | "uuid": "^8.3.2", 344 | "ws": "^7.4.5" 345 | }, 346 | "bin": { 347 | "jayson": "bin/jayson.js" 348 | }, 349 | "engines": { 350 | "node": ">=8" 351 | } 352 | }, 353 | "node_modules/json-stringify-safe": { 354 | "version": "5.0.1", 355 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 356 | "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" 357 | }, 358 | "node_modules/jsonparse": { 359 | "version": "1.3.1", 360 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", 361 | "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", 362 | "engines": [ 363 | "node >= 0.2.0" 364 | ] 365 | }, 366 | "node_modules/JSONStream": { 367 | "version": "1.3.5", 368 | "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", 369 | "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", 370 | "dependencies": { 371 | "jsonparse": "^1.2.0", 372 | "through": ">=2.2.7 <3" 373 | }, 374 | "bin": { 375 | "JSONStream": "bin.js" 376 | }, 377 | "engines": { 378 | "node": "*" 379 | } 380 | }, 381 | "node_modules/lodash": { 382 | "version": "4.17.21", 383 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 384 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 385 | }, 386 | "node_modules/node-fetch": { 387 | "version": "2.6.7", 388 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 389 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 390 | "dependencies": { 391 | "whatwg-url": "^5.0.0" 392 | }, 393 | "engines": { 394 | "node": "4.x || >=6.0.0" 395 | }, 396 | "peerDependencies": { 397 | "encoding": "^0.1.0" 398 | }, 399 | "peerDependenciesMeta": { 400 | "encoding": { 401 | "optional": true 402 | } 403 | } 404 | }, 405 | "node_modules/node-gyp-build": { 406 | "version": "4.5.0", 407 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", 408 | "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", 409 | "optional": true, 410 | "bin": { 411 | "node-gyp-build": "bin.js", 412 | "node-gyp-build-optional": "optional.js", 413 | "node-gyp-build-test": "build-test.js" 414 | } 415 | }, 416 | "node_modules/regenerator-runtime": { 417 | "version": "0.13.9", 418 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", 419 | "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" 420 | }, 421 | "node_modules/rpc-websockets": { 422 | "version": "7.5.0", 423 | "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.5.0.tgz", 424 | "integrity": "sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ==", 425 | "dependencies": { 426 | "@babel/runtime": "^7.17.2", 427 | "eventemitter3": "^4.0.7", 428 | "uuid": "^8.3.2", 429 | "ws": "^8.5.0" 430 | }, 431 | "funding": { 432 | "type": "paypal", 433 | "url": "https://paypal.me/kozjak" 434 | }, 435 | "optionalDependencies": { 436 | "bufferutil": "^4.0.1", 437 | "utf-8-validate": "^5.0.2" 438 | } 439 | }, 440 | "node_modules/rpc-websockets/node_modules/ws": { 441 | "version": "8.9.0", 442 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", 443 | "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", 444 | "engines": { 445 | "node": ">=10.0.0" 446 | }, 447 | "peerDependencies": { 448 | "bufferutil": "^4.0.1", 449 | "utf-8-validate": "^5.0.2" 450 | }, 451 | "peerDependenciesMeta": { 452 | "bufferutil": { 453 | "optional": true 454 | }, 455 | "utf-8-validate": { 456 | "optional": true 457 | } 458 | } 459 | }, 460 | "node_modules/safe-buffer": { 461 | "version": "5.2.1", 462 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 463 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 464 | "funding": [ 465 | { 466 | "type": "github", 467 | "url": "https://github.com/sponsors/feross" 468 | }, 469 | { 470 | "type": "patreon", 471 | "url": "https://www.patreon.com/feross" 472 | }, 473 | { 474 | "type": "consulting", 475 | "url": "https://feross.org/support" 476 | } 477 | ] 478 | }, 479 | "node_modules/superstruct": { 480 | "version": "0.14.2", 481 | "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz", 482 | "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==" 483 | }, 484 | "node_modules/text-encoding-utf-8": { 485 | "version": "1.0.2", 486 | "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", 487 | "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" 488 | }, 489 | "node_modules/through": { 490 | "version": "2.3.8", 491 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 492 | "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" 493 | }, 494 | "node_modules/tr46": { 495 | "version": "0.0.3", 496 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 497 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 498 | }, 499 | "node_modules/typescript": { 500 | "version": "4.8.4", 501 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 502 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 503 | "dev": true, 504 | "bin": { 505 | "tsc": "bin/tsc", 506 | "tsserver": "bin/tsserver" 507 | }, 508 | "engines": { 509 | "node": ">=4.2.0" 510 | } 511 | }, 512 | "node_modules/utf-8-validate": { 513 | "version": "5.0.9", 514 | "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz", 515 | "integrity": "sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==", 516 | "hasInstallScript": true, 517 | "optional": true, 518 | "dependencies": { 519 | "node-gyp-build": "^4.3.0" 520 | }, 521 | "engines": { 522 | "node": ">=6.14.2" 523 | } 524 | }, 525 | "node_modules/uuid": { 526 | "version": "8.3.2", 527 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 528 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 529 | "bin": { 530 | "uuid": "dist/bin/uuid" 531 | } 532 | }, 533 | "node_modules/webidl-conversions": { 534 | "version": "3.0.1", 535 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 536 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 537 | }, 538 | "node_modules/whatwg-url": { 539 | "version": "5.0.0", 540 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 541 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 542 | "dependencies": { 543 | "tr46": "~0.0.3", 544 | "webidl-conversions": "^3.0.0" 545 | } 546 | }, 547 | "node_modules/ws": { 548 | "version": "7.5.9", 549 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", 550 | "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", 551 | "engines": { 552 | "node": ">=8.3.0" 553 | }, 554 | "peerDependencies": { 555 | "bufferutil": "^4.0.1", 556 | "utf-8-validate": "^5.0.2" 557 | }, 558 | "peerDependenciesMeta": { 559 | "bufferutil": { 560 | "optional": true 561 | }, 562 | "utf-8-validate": { 563 | "optional": true 564 | } 565 | } 566 | } 567 | }, 568 | "dependencies": { 569 | "@babel/runtime": { 570 | "version": "7.19.0", 571 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", 572 | "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", 573 | "requires": { 574 | "regenerator-runtime": "^0.13.4" 575 | } 576 | }, 577 | "@noble/ed25519": { 578 | "version": "1.7.1", 579 | "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.1.tgz", 580 | "integrity": "sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==" 581 | }, 582 | "@noble/hashes": { 583 | "version": "1.1.3", 584 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz", 585 | "integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==" 586 | }, 587 | "@noble/secp256k1": { 588 | "version": "1.7.0", 589 | "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.0.tgz", 590 | "integrity": "sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw==" 591 | }, 592 | "@solana/buffer-layout": { 593 | "version": "4.0.0", 594 | "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz", 595 | "integrity": "sha512-lR0EMP2HC3+Mxwd4YcnZb0smnaDw7Bl2IQWZiTevRH5ZZBZn6VRWn3/92E3qdU4SSImJkA6IDHawOHAnx/qUvQ==", 596 | "requires": { 597 | "buffer": "~6.0.3" 598 | }, 599 | "dependencies": { 600 | "buffer": { 601 | "version": "6.0.3", 602 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 603 | "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 604 | "requires": { 605 | "base64-js": "^1.3.1", 606 | "ieee754": "^1.2.1" 607 | } 608 | } 609 | } 610 | }, 611 | "@solana/web3.js": { 612 | "version": "1.63.1", 613 | "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.63.1.tgz", 614 | "integrity": "sha512-wgEdGVK5FTS2zENxbcGSvKpGZ0jDS6BUdGu8Gn6ns0CzgJkK83u4ip3THSnBPEQ5i/jrqukg998BwV1H67+qiQ==", 615 | "requires": { 616 | "@babel/runtime": "^7.12.5", 617 | "@noble/ed25519": "^1.7.0", 618 | "@noble/hashes": "^1.1.2", 619 | "@noble/secp256k1": "^1.6.3", 620 | "@solana/buffer-layout": "^4.0.0", 621 | "bigint-buffer": "^1.1.5", 622 | "bn.js": "^5.0.0", 623 | "borsh": "^0.7.0", 624 | "bs58": "^4.0.1", 625 | "buffer": "6.0.1", 626 | "fast-stable-stringify": "^1.0.0", 627 | "jayson": "^3.4.4", 628 | "node-fetch": "2", 629 | "rpc-websockets": "^7.5.0", 630 | "superstruct": "^0.14.2" 631 | } 632 | }, 633 | "@types/connect": { 634 | "version": "3.4.35", 635 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", 636 | "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", 637 | "requires": { 638 | "@types/node": "*" 639 | } 640 | }, 641 | "@types/node": { 642 | "version": "12.20.55", 643 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", 644 | "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" 645 | }, 646 | "@types/ws": { 647 | "version": "7.4.7", 648 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", 649 | "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", 650 | "requires": { 651 | "@types/node": "*" 652 | } 653 | }, 654 | "base-x": { 655 | "version": "3.0.9", 656 | "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", 657 | "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", 658 | "requires": { 659 | "safe-buffer": "^5.0.1" 660 | } 661 | }, 662 | "base64-js": { 663 | "version": "1.5.1", 664 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 665 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 666 | }, 667 | "bigint-buffer": { 668 | "version": "1.1.5", 669 | "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", 670 | "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", 671 | "requires": { 672 | "bindings": "^1.3.0" 673 | } 674 | }, 675 | "bindings": { 676 | "version": "1.5.0", 677 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 678 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 679 | "requires": { 680 | "file-uri-to-path": "1.0.0" 681 | } 682 | }, 683 | "bn.js": { 684 | "version": "5.2.1", 685 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", 686 | "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" 687 | }, 688 | "borsh": { 689 | "version": "0.7.0", 690 | "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", 691 | "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", 692 | "requires": { 693 | "bn.js": "^5.2.0", 694 | "bs58": "^4.0.0", 695 | "text-encoding-utf-8": "^1.0.2" 696 | } 697 | }, 698 | "bs58": { 699 | "version": "4.0.1", 700 | "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", 701 | "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", 702 | "requires": { 703 | "base-x": "^3.0.2" 704 | } 705 | }, 706 | "buffer": { 707 | "version": "6.0.1", 708 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.1.tgz", 709 | "integrity": "sha512-rVAXBwEcEoYtxnHSO5iWyhzV/O1WMtkUYWlfdLS7FjU4PnSJJHEfHXi/uHPI5EwltmOA794gN3bm3/pzuctWjQ==", 710 | "requires": { 711 | "base64-js": "^1.3.1", 712 | "ieee754": "^1.2.1" 713 | } 714 | }, 715 | "bufferutil": { 716 | "version": "4.0.6", 717 | "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz", 718 | "integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==", 719 | "optional": true, 720 | "requires": { 721 | "node-gyp-build": "^4.3.0" 722 | } 723 | }, 724 | "commander": { 725 | "version": "2.20.3", 726 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 727 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 728 | }, 729 | "delay": { 730 | "version": "5.0.0", 731 | "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", 732 | "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==" 733 | }, 734 | "es6-promise": { 735 | "version": "4.2.8", 736 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 737 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" 738 | }, 739 | "es6-promisify": { 740 | "version": "5.0.0", 741 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 742 | "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", 743 | "requires": { 744 | "es6-promise": "^4.0.3" 745 | } 746 | }, 747 | "eventemitter3": { 748 | "version": "4.0.7", 749 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", 750 | "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" 751 | }, 752 | "eyes": { 753 | "version": "0.1.8", 754 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 755 | "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==" 756 | }, 757 | "fast-stable-stringify": { 758 | "version": "1.0.0", 759 | "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", 760 | "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" 761 | }, 762 | "file-uri-to-path": { 763 | "version": "1.0.0", 764 | "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 765 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" 766 | }, 767 | "ieee754": { 768 | "version": "1.2.1", 769 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 770 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 771 | }, 772 | "isomorphic-ws": { 773 | "version": "4.0.1", 774 | "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", 775 | "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", 776 | "requires": {} 777 | }, 778 | "jayson": { 779 | "version": "3.7.0", 780 | "resolved": "https://registry.npmjs.org/jayson/-/jayson-3.7.0.tgz", 781 | "integrity": "sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==", 782 | "requires": { 783 | "@types/connect": "^3.4.33", 784 | "@types/node": "^12.12.54", 785 | "@types/ws": "^7.4.4", 786 | "commander": "^2.20.3", 787 | "delay": "^5.0.0", 788 | "es6-promisify": "^5.0.0", 789 | "eyes": "^0.1.8", 790 | "isomorphic-ws": "^4.0.1", 791 | "json-stringify-safe": "^5.0.1", 792 | "JSONStream": "^1.3.5", 793 | "lodash": "^4.17.20", 794 | "uuid": "^8.3.2", 795 | "ws": "^7.4.5" 796 | } 797 | }, 798 | "json-stringify-safe": { 799 | "version": "5.0.1", 800 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 801 | "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" 802 | }, 803 | "jsonparse": { 804 | "version": "1.3.1", 805 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", 806 | "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==" 807 | }, 808 | "JSONStream": { 809 | "version": "1.3.5", 810 | "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", 811 | "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", 812 | "requires": { 813 | "jsonparse": "^1.2.0", 814 | "through": ">=2.2.7 <3" 815 | } 816 | }, 817 | "lodash": { 818 | "version": "4.17.21", 819 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 820 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 821 | }, 822 | "node-fetch": { 823 | "version": "2.6.7", 824 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 825 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 826 | "requires": { 827 | "whatwg-url": "^5.0.0" 828 | } 829 | }, 830 | "node-gyp-build": { 831 | "version": "4.5.0", 832 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", 833 | "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", 834 | "optional": true 835 | }, 836 | "regenerator-runtime": { 837 | "version": "0.13.9", 838 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", 839 | "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" 840 | }, 841 | "rpc-websockets": { 842 | "version": "7.5.0", 843 | "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.5.0.tgz", 844 | "integrity": "sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ==", 845 | "requires": { 846 | "@babel/runtime": "^7.17.2", 847 | "bufferutil": "^4.0.1", 848 | "eventemitter3": "^4.0.7", 849 | "utf-8-validate": "^5.0.2", 850 | "uuid": "^8.3.2", 851 | "ws": "^8.5.0" 852 | }, 853 | "dependencies": { 854 | "ws": { 855 | "version": "8.9.0", 856 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", 857 | "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", 858 | "requires": {} 859 | } 860 | } 861 | }, 862 | "safe-buffer": { 863 | "version": "5.2.1", 864 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 865 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 866 | }, 867 | "superstruct": { 868 | "version": "0.14.2", 869 | "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz", 870 | "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==" 871 | }, 872 | "text-encoding-utf-8": { 873 | "version": "1.0.2", 874 | "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", 875 | "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" 876 | }, 877 | "through": { 878 | "version": "2.3.8", 879 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 880 | "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" 881 | }, 882 | "tr46": { 883 | "version": "0.0.3", 884 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 885 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 886 | }, 887 | "typescript": { 888 | "version": "4.8.4", 889 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 890 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 891 | "dev": true 892 | }, 893 | "utf-8-validate": { 894 | "version": "5.0.9", 895 | "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz", 896 | "integrity": "sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==", 897 | "optional": true, 898 | "requires": { 899 | "node-gyp-build": "^4.3.0" 900 | } 901 | }, 902 | "uuid": { 903 | "version": "8.3.2", 904 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 905 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" 906 | }, 907 | "webidl-conversions": { 908 | "version": "3.0.1", 909 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 910 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 911 | }, 912 | "whatwg-url": { 913 | "version": "5.0.0", 914 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 915 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 916 | "requires": { 917 | "tr46": "~0.0.3", 918 | "webidl-conversions": "^3.0.0" 919 | } 920 | }, 921 | "ws": { 922 | "version": "7.5.9", 923 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", 924 | "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", 925 | "requires": {} 926 | } 927 | } 928 | } 929 | -------------------------------------------------------------------------------- /examples/sol-contract/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "invoke.js", 6 | "scripts": { 7 | "build": "npx tsc" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "typescript": "^4.8.4" 14 | }, 15 | "dependencies": { 16 | "@solana/web3.js": "^1.56.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/sol-contract/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true 8 | }, 9 | "lib": ["es2015"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/sol-contract/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | //! Program entrypoint 2 | //! Every solana program has an entry point function with 3 parameters: 3 | //! the program ID, the accounts being touched by this program, 4 | //! and a byte array as the instruction data. 5 | 6 | use solana_program::account_info::AccountInfo; 7 | use solana_program::entrypoint; 8 | use solana_program::entrypoint::ProgramResult; 9 | use solana_program::pubkey::Pubkey; 10 | 11 | entrypoint!(process_instruction); 12 | fn process_instruction( 13 | program_id: &Pubkey, 14 | accounts: &[AccountInfo], 15 | instruction_data: &[u8], 16 | ) -> ProgramResult { 17 | crate::processor::process_instruction(program_id, accounts, instruction_data) 18 | } 19 | -------------------------------------------------------------------------------- /examples/sol-contract/src/instruction.rs: -------------------------------------------------------------------------------- 1 | //! Program instructions 2 | //! A solana program contains a number of instructions. 3 | //! There are 2 instructions in this example: 4 | //! Init{} initializing some loan information and 5 | //! Loan2Value{} checking the loan-to-value ratio of the loan. 6 | 7 | use borsh::{ 8 | BorshDeserialize, 9 | BorshSerialize, 10 | }; 11 | 12 | #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] 13 | pub enum ExampleInstructions { 14 | Init {}, 15 | Loan2Value { 16 | loan_qty: i64, 17 | collateral_qty: i64, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /examples/sol-contract/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This file specifies the 4 modules of this example program. 2 | 3 | pub mod entrypoint; 4 | pub mod instruction; 5 | pub mod processor; 6 | pub mod state; 7 | -------------------------------------------------------------------------------- /examples/sol-contract/src/processor.rs: -------------------------------------------------------------------------------- 1 | //! Program instruction processor 2 | //! Only the program admin can issue the Init instruction. 3 | //! And anyone can check the loan with the Loan2Value instruction. 4 | 5 | use solana_program::account_info::{ 6 | next_account_info, 7 | AccountInfo, 8 | }; 9 | use solana_program::entrypoint::ProgramResult; 10 | use solana_program::msg; 11 | use solana_program::program_error::ProgramError; 12 | use solana_program::program_memory::sol_memcpy; 13 | use solana_program::pubkey::Pubkey; 14 | use solana_program::sysvar::clock::Clock; 15 | use solana_program::sysvar::Sysvar; 16 | 17 | use borsh::{ 18 | BorshDeserialize, 19 | BorshSerialize, 20 | }; 21 | use pyth_sdk_solana::state::SolanaPriceAccount; 22 | 23 | use crate::instruction::ExampleInstructions; 24 | use crate::state::AdminConfig; 25 | 26 | pub fn process_instruction( 27 | program_id: &Pubkey, 28 | accounts: &[AccountInfo], 29 | input: &[u8], 30 | ) -> ProgramResult { 31 | let account_iter = &mut accounts.iter(); 32 | let signer = next_account_info(account_iter)?; 33 | let admin_config_account = next_account_info(account_iter)?; 34 | let pyth_loan_account = next_account_info(account_iter)?; 35 | let pyth_collateral_account = next_account_info(account_iter)?; 36 | 37 | let instruction = ExampleInstructions::try_from_slice(input)?; 38 | match instruction { 39 | ExampleInstructions::Init {} => { 40 | // Only an authorized key should be able to configure the price feed id for each asset 41 | if !(signer.key == program_id && signer.is_signer) { 42 | return Err(ProgramError::Custom(0)); 43 | } 44 | 45 | let mut config = AdminConfig::try_from_slice(&admin_config_account.try_borrow_data()?)?; 46 | 47 | if config.is_initialized { 48 | return Err(ProgramError::Custom(1)); 49 | } 50 | 51 | config.is_initialized = true; 52 | config.loan_price_feed_id = *pyth_loan_account.key; 53 | config.collateral_price_feed_id = *pyth_collateral_account.key; 54 | 55 | // Make sure these Pyth price accounts can be loaded 56 | SolanaPriceAccount::account_info_to_feed(pyth_loan_account)?; 57 | SolanaPriceAccount::account_info_to_feed(pyth_collateral_account)?; 58 | 59 | let config_data = config.try_to_vec()?; 60 | let config_dst = &mut admin_config_account.try_borrow_mut_data()?; 61 | sol_memcpy(config_dst, &config_data, 1 + 32 + 32); 62 | Ok(()) 63 | } 64 | 65 | ExampleInstructions::Loan2Value { 66 | loan_qty, 67 | collateral_qty, 68 | } => { 69 | msg!("Loan quantity is {}.", loan_qty); 70 | msg!("Collateral quantity is {}.", collateral_qty); 71 | 72 | let config = AdminConfig::try_from_slice(&admin_config_account.try_borrow_data()?)?; 73 | 74 | if !config.is_initialized { 75 | return Err(ProgramError::Custom(1)); 76 | } 77 | 78 | if config.loan_price_feed_id != *pyth_loan_account.key 79 | || config.collateral_price_feed_id != *pyth_collateral_account.key 80 | { 81 | return Err(ProgramError::Custom(2)); 82 | } 83 | 84 | // With high confidence, the maximum value of the loan is 85 | // (price + conf) * loan_qty * 10 ^ (expo). 86 | // Here is more explanation on confidence interval in Pyth: 87 | // https://docs.pyth.network/consume-data/best-practices 88 | let feed1 = SolanaPriceAccount::account_info_to_feed(pyth_loan_account)?; 89 | let current_timestamp1 = Clock::get()?.unix_timestamp; 90 | let result1 = feed1 91 | .get_price_no_older_than(current_timestamp1, 60) 92 | .ok_or(ProgramError::Custom(3))?; 93 | let loan_max_price = result1 94 | .price 95 | .checked_add(result1.conf as i64) 96 | .ok_or(ProgramError::Custom(4))?; 97 | let mut loan_max_value = loan_max_price 98 | .checked_mul(loan_qty) 99 | .ok_or(ProgramError::Custom(4))?; 100 | msg!( 101 | "The maximum loan value is {} * 10^({}).", 102 | loan_max_value, 103 | result1.expo 104 | ); 105 | 106 | // With high confidence, the minimum value of the collateral is 107 | // (price - conf) * collateral_qty * 10 ^ (expo). 108 | // Here is more explanation on confidence interval in Pyth: 109 | // https://docs.pyth.network/consume-data/best-practices 110 | let feed2 = SolanaPriceAccount::account_info_to_feed(pyth_collateral_account)?; 111 | let current_timestamp2 = Clock::get()?.unix_timestamp; 112 | let result2 = feed2 113 | .get_price_no_older_than(current_timestamp2, 60) 114 | .ok_or(ProgramError::Custom(3))?; 115 | let collateral_min_price = result2 116 | .price 117 | .checked_sub(result2.conf as i64) 118 | .ok_or(ProgramError::Custom(4))?; 119 | let mut collateral_min_value = collateral_min_price 120 | .checked_mul(collateral_qty) 121 | .ok_or(ProgramError::Custom(4))?; 122 | msg!( 123 | "The minimum collateral value is {} * 10^({}).", 124 | collateral_min_value, 125 | result2.expo 126 | ); 127 | 128 | // If the loan and collateral prices use different exponent, 129 | // normalize the value. 130 | if result1.expo > result2.expo { 131 | let normalize = 10_i64 132 | .checked_pow((result1.expo - result2.expo) as u32) 133 | .ok_or(ProgramError::Custom(4))?; 134 | collateral_min_value = collateral_min_value 135 | .checked_mul(normalize) 136 | .ok_or(ProgramError::Custom(4))?; 137 | } else if result1.expo < result2.expo { 138 | let normalize = 10_i64 139 | .checked_pow((result2.expo - result1.expo) as u32) 140 | .ok_or(ProgramError::Custom(4))?; 141 | loan_max_value = loan_max_value 142 | .checked_mul(normalize) 143 | .ok_or(ProgramError::Custom(4))?; 144 | } 145 | 146 | // Check whether the value of the collateral is higher. 147 | if collateral_min_value > loan_max_value { 148 | msg!("The value of the collateral is higher."); 149 | Ok(()) 150 | } else { 151 | msg!("The value of the loan is higher!"); 152 | Err(ProgramError::Custom(5)) 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /examples/sol-contract/src/state.rs: -------------------------------------------------------------------------------- 1 | //! Program states 2 | //! A data account would store an AdminConfig structure for instructions. 3 | //! This file contains the serialization / deserialization of AdminConfig. 4 | 5 | use borsh::{ 6 | BorshDeserialize, 7 | BorshSerialize, 8 | }; 9 | use solana_program::pubkey::Pubkey; 10 | 11 | // loan_price_feed_id and collateral_price_feed_id are the 12 | // Pyth price accounts for the loan and collateral tokens 13 | #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] 14 | pub struct AdminConfig { 15 | pub is_initialized: bool, 16 | pub loan_price_feed_id: Pubkey, 17 | pub collateral_price_feed_id: Pubkey, 18 | } 19 | -------------------------------------------------------------------------------- /pyth-sdk-solana/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyth-sdk-solana" 3 | version = "0.10.5" 4 | authors = ["Pyth Data Foundation"] 5 | workspace = "../" 6 | edition = "2018" 7 | license = "Apache-2.0" 8 | homepage = "https://pyth.network" 9 | repository = "https://github.com/pyth-network/pyth-sdk-rs" 10 | description = "pyth price oracle data structures and example usage" 11 | keywords = ["pyth", "solana", "oracle"] 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | pyth-sdk.workspace = true 16 | 17 | solana-program.workspace = true 18 | borsh.workspace = true 19 | borsh-derive.workspace = true 20 | bytemuck = { version = "1.7.2", features = ["derive"] } 21 | num-derive = "0.3" 22 | num-traits = "0.2" 23 | thiserror = "1.0" 24 | serde = { workspace = true, features = ["derive"] } 25 | 26 | [dev-dependencies] 27 | solana-client = ">= 1.9" 28 | solana-sdk = ">= 1.9" 29 | 30 | [lib] 31 | crate-type = ["cdylib", "lib"] 32 | 33 | [package.metadata.docs.rs] 34 | targets = ["x86_64-unknown-linux-gnu"] 35 | -------------------------------------------------------------------------------- /pyth-sdk-solana/README.md: -------------------------------------------------------------------------------- 1 | # Pyth Network Solana SDK 2 | 3 | This crate provides utilities for reading price feeds from the [pyth.network](https://pyth.network/) oracle on the Solana network. 4 | It also includes several [off-chain example programs](examples/). 5 | 6 | ## Installation 7 | 8 | Add a dependency to your Cargo.toml: 9 | 10 | ```toml 11 | [dependencies] 12 | pyth-sdk-solana="" 13 | ``` 14 | 15 | See [pyth-sdk-solana on crates.io](https://crates.io/crates/pyth-sdk-solana/) to get the latest version of the library. 16 | 17 | ## Usage 18 | 19 | Pyth Network stores its price feeds in a collection of Solana accounts of various types: 20 | * Price accounts store the current price for a product 21 | * Product accounts store metadata about a product, such as its symbol (e.g., "BTC/USD"). 22 | * Mapping accounts store a listing of all Pyth accounts 23 | 24 | Most users of this SDK only need to access the content of price accounts; the other two account types are implementation details of the oracle. 25 | Applications can obtain the content of these accounts in two different ways: 26 | * On-chain programs should pass these accounts to the instructions that require price feeds. 27 | * Off-chain programs can access these accounts using the Solana RPC client (as in the [eth price example program](examples/eth_price.rs)). 28 | 29 | The [pyth.network](https://pyth.network/developers/price-feed-ids#solana-mainnet-beta) website can be used to identify the public keys of each price feed's price account (e.g. Crypto.BTC/USD). 30 | 31 | ### On-chain 32 | 33 | On-chain applications should pass the relevant Pyth Network price account to the Solana instruction that consumes it. 34 | This price account will be represented as an `AccountInfo` in the code for the Solana instruction. 35 | The `load_price_feed_from_account_info` function will construct a `PriceFeed` struct from `AccountInfo`: 36 | 37 | ```rust 38 | use pyth_sdk_solana::{load_price_feed_from_account_info, PriceFeed}; 39 | 40 | const STALENESS_THRESHOLD : u64 = 60; // staleness threshold in seconds 41 | let price_account_info: AccountInfo = ...; 42 | let price_feed: PriceFeed = load_price_feed_from_account_info( &price_account_info ).unwrap(); 43 | let current_timestamp = Clock::get()?.unix_timestamp; 44 | let current_price: Price = price_feed.get_price_no_older_than(current_timestamp, STALENESS_THRESHOLD).unwrap(); 45 | msg!("price: ({} +- {}) x 10^{}", current_price.price, current_price.conf, current_price.expo); 46 | ``` 47 | 48 | The `PriceFeed` object returned by `load_price_feed_from_account_info` contains all currently-available pricing information about the product. 49 | This struct also has some useful functions for manipulating and combining prices; see the [common SDK documentation](../pyth-sdk) for more details. 50 | 51 | The function `get_price_no_older_than` takes in an `age` in seconds. If the current on-chain aggregate is older than `current_timestamp - age`, `get_price_no_older_than` will return `None`. 52 | 53 | Note that your application should also validate the address of the passed-in price account before using it. 54 | Otherwise, an attacker could pass in a different account and set the price to an arbitrary value. 55 | 56 | ### Off-chain 57 | 58 | Off-chain applications can read the current value of a Pyth Network price account using the Solana RPC client. 59 | This client will return the content of the account as an `Account` struct. 60 | The `load_price_feed_from_account` function will construct a `PriceFeed` struct from `Account`: 61 | 62 | ```rust 63 | use pyth_sdk_solana::{load_price_feed_from_account, PriceFeed}; 64 | 65 | const STALENESS_THRESHOLD : u64 = 60; // staleness threshold in seconds 66 | let current_time = SystemTime::now() 67 | .duration_since(UNIX_EPOCH) 68 | .unwrap() 69 | .as_secs() as i64; 70 | 71 | let price_key: Pubkey = ...; 72 | let mut price_account: Account = clnt.get_account(&price_key).unwrap(); 73 | let price_feed: PriceFeed = load_price_feed_from_account( &price_key, &mut price_account ).unwrap(); 74 | let current_price: Price = price_feed.get_price_no_older_than(current_time, STALENESS_THRESHOLD).unwrap(); 75 | println!("price: ({} +- {}) x 10^{}", current_price.price, current_price.conf, current_price.expo); 76 | ``` 77 | 78 | ## Low-Level Solana Account Structure 79 | 80 | > :warning: The Solana account structure is an internal API that is subject to change. Prefer to use `load_price_feed_*` when possible. 81 | 82 | This library also provides several `load_*` methods that allow users to translate the binary data in each account into an appropriate struct: 83 | 84 | ```rust 85 | use pyth_sdk_solana::state::*; 86 | 87 | // replace with account data, either passed to on-chain program or from RPC node 88 | let price_account_data: Vec = ...; 89 | let price_account: &PriceAccount = load_price_account( &price_account_data ).unwrap(); 90 | 91 | let product_account_data: Vec = ...; 92 | let product_account: &ProductAccount = load_product_account( &product_account_data ).unwrap(); 93 | 94 | let mapping_account_data: Vec = ...; 95 | let mapping_account: &MappingAccount = load_mapping_account( &mapping_account_data ).unwrap(); 96 | ``` 97 | 98 | For more information on the different types of Pyth accounts, see the [account structure documentation](https://docs.pyth.network/how-pyth-works/account-structure). 99 | 100 | ## Off-chain Example Programs 101 | 102 | The example [eth_price](examples/eth_price.rs) program prints the product reference data and current price information for Pyth on pythnet. You can use the same example and replace the url with the relevant Solana cluster urls to get the same information for Solana clusters. 103 | Run the following commands to try this example program: 104 | 105 | ``` 106 | cargo build --examples 107 | cargo run --example eth_price 108 | ``` 109 | 110 | The output of this command is price of ETH/USD over time, such as: 111 | 112 | ``` 113 | .....ETH/USD..... 114 | status .......... Trading 115 | num_publishers .. 19 116 | price ........... 291958500000 x 10^-8 117 | conf ............ 163920000 x 10^-8 118 | ema_price ....... 291343470000 x 10^-8 119 | ema_conf ........ 98874533 x 10^-8 120 | ``` 121 | 122 | For [an example](examples/get_accounts.rs) of using Solana Account structure please run: 123 | ``` 124 | cargo run --example get_accounts 125 | ``` 126 | 127 | The output of this command is a listing of Pyth's accounts, such as: 128 | 129 | ``` 130 | product_account .. 6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8 131 | symbol.......... SRM/USD 132 | asset_type...... Crypto 133 | quote_currency.. USD 134 | description..... SRM/USD 135 | generic_symbol.. SRMUSD 136 | base............ SRM 137 | price_account .. 992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs 138 | price ........ 7398000000 139 | conf ......... 3200000 140 | price_type ... price 141 | exponent ..... -9 142 | status ....... trading 143 | corp_act ..... nocorpact 144 | num_qt ....... 1 145 | valid_slot ... 91340924 146 | publish_slot . 91340925 147 | ema_price .... 7426390900 148 | ema_conf ..... 2259870 149 | ``` 150 | 151 | ## Development 152 | 153 | This library can be built for either your native platform or in BPF (used by Solana programs). 154 | Use `cargo build` / `cargo test` to build and test natively. 155 | 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). 156 | -------------------------------------------------------------------------------- /pyth-sdk-solana/examples/eth_price.rs: -------------------------------------------------------------------------------- 1 | // example usage of reading pyth price from solana/pythnet price account 2 | 3 | use pyth_sdk_solana::state::SolanaPriceAccount; 4 | use solana_client::rpc_client::RpcClient; 5 | use solana_program::pubkey::Pubkey; 6 | use std::str::FromStr; 7 | use std::time::{ 8 | SystemTime, 9 | UNIX_EPOCH, 10 | }; 11 | use std::{ 12 | thread, 13 | time, 14 | }; 15 | 16 | 17 | fn main() { 18 | let url = "http:/pythnet.rpcpool.com"; 19 | // Pyth ETH/USD price account on pythnet (can be found on https://pyth.network/developers/price-feed-ids#solana-mainnet-beta which has the same price feed IDs as pythnet) 20 | let key = "JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB"; 21 | let clnt = RpcClient::new(url.to_string()); 22 | let eth_price_key = Pubkey::from_str(key).unwrap(); 23 | 24 | loop { 25 | // get price data from key 26 | let mut eth_price_account = clnt.get_account(ð_price_key).unwrap(); 27 | let eth_price_feed = 28 | SolanaPriceAccount::account_to_feed(ð_price_key, &mut eth_price_account).unwrap(); 29 | 30 | println!(".....ETH/USD....."); 31 | 32 | let current_time = SystemTime::now() 33 | .duration_since(UNIX_EPOCH) 34 | .unwrap() 35 | .as_secs() as i64; 36 | 37 | let maybe_price = eth_price_feed.get_price_no_older_than(current_time, 60); 38 | match maybe_price { 39 | Some(p) => { 40 | println!("price ........... {} x 10^{}", p.price, p.expo); 41 | println!("conf ............ {} x 10^{}", p.conf, p.expo); 42 | } 43 | None => { 44 | println!("price ........... unavailable"); 45 | println!("conf ............ unavailable"); 46 | } 47 | } 48 | 49 | 50 | let maybe_ema_price = eth_price_feed.get_ema_price_no_older_than(current_time, 60); 51 | match maybe_ema_price { 52 | Some(ema_price) => { 53 | println!( 54 | "ema_price ....... {} x 10^{}", 55 | ema_price.price, ema_price.expo 56 | ); 57 | println!( 58 | "ema_conf ........ {} x 10^{}", 59 | ema_price.conf, ema_price.expo 60 | ); 61 | } 62 | None => { 63 | println!("ema_price ....... unavailable"); 64 | println!("ema_conf ........ unavailable"); 65 | } 66 | } 67 | 68 | println!(); 69 | 70 | thread::sleep(time::Duration::from_secs(1)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pyth-sdk-solana/examples/get_accounts.rs: -------------------------------------------------------------------------------- 1 | // example usage of pyth solana account structure 2 | // bootstrap all product and pricing accounts from root mapping account 3 | // It is adviced to use Price directly wherever possible as described in eth_price example. 4 | // Please use account structure only if you need it. 5 | 6 | use pyth_sdk_solana::state::{ 7 | load_mapping_account, 8 | load_price_account, 9 | load_product_account, 10 | CorpAction, 11 | PriceType, 12 | SolanaPriceAccount, 13 | }; 14 | use solana_client::rpc_client::RpcClient; 15 | use solana_program::pubkey::Pubkey; 16 | use std::str::FromStr; 17 | use std::time::{ 18 | SystemTime, 19 | UNIX_EPOCH, 20 | }; 21 | 22 | fn get_price_type(ptype: &PriceType) -> &'static str { 23 | match ptype { 24 | PriceType::Unknown => "unknown", 25 | PriceType::Price => "price", 26 | } 27 | } 28 | 29 | fn get_corp_act(cact: &CorpAction) -> &'static str { 30 | match cact { 31 | CorpAction::NoCorpAct => "nocorpact", 32 | } 33 | } 34 | 35 | fn main() { 36 | // get pyth mapping account 37 | let url = "http://api.devnet.solana.com"; 38 | let key = "BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2"; 39 | let clnt = RpcClient::new(url.to_string()); 40 | let mut akey = Pubkey::from_str(key).unwrap(); 41 | 42 | loop { 43 | // get Mapping account from key 44 | let map_data = clnt.get_account_data(&akey).unwrap(); 45 | let map_acct = load_mapping_account(&map_data).unwrap(); 46 | 47 | // iget and print each Product in Mapping directory 48 | let mut i = 0; 49 | for prod_pkey in &map_acct.products { 50 | let prod_data = clnt.get_account_data(prod_pkey).unwrap(); 51 | let prod_acct = load_product_account(&prod_data).unwrap(); 52 | 53 | // print key and reference data for this Product 54 | println!("product_account .. {:?}", prod_pkey); 55 | for (key, val) in prod_acct.iter() { 56 | if !key.is_empty() { 57 | println!(" {:.<16} {}", key, val); 58 | } 59 | } 60 | 61 | // print all Prices that correspond to this Product 62 | if prod_acct.px_acc != Pubkey::default() { 63 | let mut px_pkey = prod_acct.px_acc; 64 | loop { 65 | let price_data = clnt.get_account_data(&px_pkey).unwrap(); 66 | let price_account: &SolanaPriceAccount = 67 | load_price_account(&price_data).unwrap(); 68 | let price_feed = price_account.to_price_feed(&px_pkey); 69 | 70 | println!(" price_account .. {:?}", px_pkey); 71 | 72 | let current_time = SystemTime::now() 73 | .duration_since(UNIX_EPOCH) 74 | .unwrap() 75 | .as_secs() as i64; 76 | 77 | let maybe_price = price_feed.get_price_no_older_than(current_time, 60); 78 | match maybe_price { 79 | Some(p) => { 80 | println!(" price ........ {} x 10^{}", p.price, p.expo); 81 | println!(" conf ......... {} x 10^{}", p.conf, p.expo); 82 | } 83 | None => { 84 | println!(" price ........ unavailable"); 85 | println!(" conf ......... unavailable"); 86 | } 87 | } 88 | 89 | println!( 90 | " price_type ... {}", 91 | get_price_type(&price_account.ptype) 92 | ); 93 | println!( 94 | " corp_act ..... {}", 95 | get_corp_act(&price_account.agg.corp_act) 96 | ); 97 | 98 | println!(" num_qt ....... {}", price_account.num_qt); 99 | println!(" valid_slot ... {}", price_account.valid_slot); 100 | println!(" publish_slot . {}", price_account.agg.pub_slot); 101 | 102 | let maybe_ema_price = price_feed.get_ema_price_no_older_than(current_time, 60); 103 | match maybe_ema_price { 104 | Some(ema_price) => { 105 | println!( 106 | " ema_price .... {} x 10^{}", 107 | ema_price.price, ema_price.expo 108 | ); 109 | println!( 110 | " ema_conf ..... {} x 10^{}", 111 | ema_price.conf, ema_price.expo 112 | ); 113 | } 114 | None => { 115 | println!(" ema_price .... unavailable"); 116 | println!(" ema_conf ..... unavailable"); 117 | } 118 | } 119 | 120 | // go to next price account in list 121 | if price_account.next != Pubkey::default() { 122 | px_pkey = price_account.next; 123 | } else { 124 | break; 125 | } 126 | } 127 | } 128 | // go to next product 129 | i += 1; 130 | if i == map_acct.num { 131 | break; 132 | } 133 | } 134 | 135 | // go to next Mapping account in list 136 | if map_acct.next == Pubkey::default() { 137 | break; 138 | } 139 | akey = map_acct.next; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /pyth-sdk-solana/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 | -------------------------------------------------------------------------------- /pyth-sdk-solana/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-sdk-solana/) for documentation and example usage. 4 | 5 | pub use self::error::PythError; 6 | 7 | mod error; 8 | pub mod state; 9 | 10 | use solana_program::account_info::{ 11 | Account, 12 | AccountInfo, 13 | IntoAccountInfo, 14 | }; 15 | use solana_program::pubkey::Pubkey; 16 | 17 | use state::{ 18 | load_price_account, 19 | GenericPriceAccount, 20 | SolanaPriceAccount, 21 | }; 22 | 23 | pub use pyth_sdk::{ 24 | Price, 25 | PriceFeed, 26 | PriceIdentifier, 27 | ProductIdentifier, 28 | }; 29 | 30 | /// Maximum valid slot period before price is considered to be stale. 31 | pub const VALID_SLOT_PERIOD: u64 = 25; 32 | 33 | /// Loads Pyth Feed Price from Price Account Info. 34 | #[deprecated(note = "solana-specific, use SolanaPriceAccount::account_info_to_feed instead.")] 35 | pub fn load_price_feed_from_account_info( 36 | price_account_info: &AccountInfo, 37 | ) -> Result { 38 | SolanaPriceAccount::account_info_to_feed(price_account_info) 39 | } 40 | 41 | /// Loads Pyth Price Feed from Account when using Solana Client. 42 | /// 43 | /// It is a helper function which constructs Account Info when reading Account in clients. 44 | #[deprecated(note = "solana-specific, use SolanaPriceAccount::account_to_feed instead.")] 45 | pub fn load_price_feed_from_account( 46 | price_key: &Pubkey, 47 | price_account: &mut impl Account, 48 | ) -> Result { 49 | SolanaPriceAccount::account_to_feed(price_key, price_account) 50 | } 51 | 52 | impl GenericPriceAccount 53 | where 54 | T: Default, 55 | T: Copy, 56 | { 57 | pub fn account_info_to_feed(price_account_info: &AccountInfo) -> Result { 58 | load_price_account::( 59 | *price_account_info 60 | .try_borrow_data() 61 | .map_err(|_| PythError::InvalidAccountData)?, 62 | ) 63 | .map(|acc| acc.to_price_feed(price_account_info.key)) 64 | } 65 | 66 | pub fn account_to_feed( 67 | price_key: &Pubkey, 68 | price_account: &mut impl Account, 69 | ) -> Result { 70 | let price_account_info = (price_key, price_account).into_account_info(); 71 | Self::account_info_to_feed(&price_account_info) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pyth-sdk-solana/src/state.rs: -------------------------------------------------------------------------------- 1 | //! Structures and functions for interacting with Solana on-chain account data. 2 | 3 | use borsh::{ 4 | BorshDeserialize, 5 | BorshSerialize, 6 | }; 7 | use bytemuck::{ 8 | cast_slice, 9 | from_bytes, 10 | try_cast_slice, 11 | Pod, 12 | PodCastError, 13 | Zeroable, 14 | }; 15 | use pyth_sdk::{ 16 | PriceIdentifier, 17 | UnixTimestamp, 18 | }; 19 | use solana_program::clock::Clock; 20 | use solana_program::pubkey::Pubkey; 21 | use std::cmp::min; 22 | use std::mem::size_of; 23 | 24 | pub use pyth_sdk::{ 25 | Price, 26 | PriceFeed, 27 | }; 28 | 29 | use crate::PythError; 30 | 31 | pub const MAGIC: u32 = 0xa1b2c3d4; 32 | pub const VERSION_2: u32 = 2; 33 | pub const VERSION: u32 = VERSION_2; 34 | pub const MAP_TABLE_SIZE: usize = 5000; 35 | pub const PROD_ACCT_SIZE: usize = 512; 36 | pub const PROD_HDR_SIZE: usize = 48; 37 | pub const PROD_ATTR_SIZE: usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; 38 | 39 | /// The type of Pyth account determines what data it contains 40 | #[derive( 41 | Copy, 42 | Clone, 43 | Debug, 44 | PartialEq, 45 | Eq, 46 | BorshSerialize, 47 | BorshDeserialize, 48 | serde::Serialize, 49 | serde::Deserialize, 50 | Default, 51 | )] 52 | #[repr(u8)] 53 | pub enum AccountType { 54 | #[default] 55 | Unknown, 56 | Mapping, 57 | Product, 58 | Price, 59 | } 60 | 61 | /// Status of any ongoing corporate actions. 62 | /// (still undergoing dev) 63 | #[derive( 64 | Copy, 65 | Clone, 66 | Debug, 67 | PartialEq, 68 | Eq, 69 | BorshSerialize, 70 | BorshDeserialize, 71 | serde::Serialize, 72 | serde::Deserialize, 73 | Default, 74 | )] 75 | #[repr(u8)] 76 | pub enum CorpAction { 77 | #[default] 78 | NoCorpAct, 79 | } 80 | 81 | /// The type of prices associated with a product -- each product may have multiple price feeds of 82 | /// different types. 83 | #[derive( 84 | Copy, 85 | Clone, 86 | Debug, 87 | PartialEq, 88 | Eq, 89 | BorshSerialize, 90 | BorshDeserialize, 91 | serde::Serialize, 92 | serde::Deserialize, 93 | Default, 94 | )] 95 | #[repr(u8)] 96 | pub enum PriceType { 97 | #[default] 98 | Unknown, 99 | Price, 100 | } 101 | 102 | /// Represents availability status of a price feed. 103 | #[derive( 104 | Copy, 105 | Clone, 106 | Debug, 107 | PartialEq, 108 | Eq, 109 | BorshSerialize, 110 | BorshDeserialize, 111 | serde::Serialize, 112 | serde::Deserialize, 113 | Default, 114 | )] 115 | #[repr(u8)] 116 | pub enum PriceStatus { 117 | /// The price feed is not currently updating for an unknown reason. 118 | #[default] 119 | Unknown, 120 | /// The price feed is updating as expected. 121 | Trading, 122 | /// The price feed is not currently updating because trading in the product has been halted. 123 | Halted, 124 | /// The price feed is not currently updating because an auction is setting the price. 125 | Auction, 126 | /// A price component can be ignored if the confidence interval is too wide 127 | Ignored, 128 | } 129 | 130 | impl std::fmt::Display for PriceStatus { 131 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 132 | write!( 133 | f, 134 | "{}", 135 | match self { 136 | Self::Unknown => "unknown", 137 | Self::Trading => "trading", 138 | Self::Halted => "halted", 139 | Self::Auction => "auction", 140 | Self::Ignored => "ignored", 141 | } 142 | ) 143 | } 144 | } 145 | 146 | /// Mapping accounts form a linked-list containing the listing of all products on Pyth. 147 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 148 | #[repr(C)] 149 | pub struct MappingAccount { 150 | /// pyth magic number 151 | pub magic: u32, 152 | /// program version 153 | pub ver: u32, 154 | /// account type 155 | pub atype: u32, 156 | /// account used size 157 | pub size: u32, 158 | /// number of product accounts 159 | pub num: u32, 160 | pub unused: u32, 161 | /// next mapping account (if any) 162 | pub next: Pubkey, 163 | pub products: [Pubkey; MAP_TABLE_SIZE], 164 | } 165 | 166 | #[cfg(target_endian = "little")] 167 | unsafe impl Zeroable for MappingAccount { 168 | } 169 | 170 | #[cfg(target_endian = "little")] 171 | unsafe impl Pod for MappingAccount { 172 | } 173 | 174 | /// Product accounts contain metadata for a single product, such as its symbol ("Crypto.BTC/USD") 175 | /// and its base/quote currencies. 176 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 177 | #[repr(C)] 178 | pub struct ProductAccount { 179 | /// pyth magic number 180 | pub magic: u32, 181 | /// program version 182 | pub ver: u32, 183 | /// account type 184 | pub atype: u32, 185 | /// price account size 186 | pub size: u32, 187 | /// first price account in list 188 | pub px_acc: Pubkey, 189 | /// key/value pairs of reference attr. 190 | pub attr: [u8; PROD_ATTR_SIZE], 191 | } 192 | 193 | impl ProductAccount { 194 | pub fn iter(&self) -> AttributeIter { 195 | AttributeIter { 196 | attrs: &self.attr[..min( 197 | (self.size as usize).saturating_sub(PROD_HDR_SIZE), 198 | PROD_ATTR_SIZE, 199 | )], 200 | } 201 | } 202 | } 203 | 204 | #[cfg(target_endian = "little")] 205 | unsafe impl Zeroable for ProductAccount { 206 | } 207 | 208 | #[cfg(target_endian = "little")] 209 | unsafe impl Pod for ProductAccount { 210 | } 211 | 212 | /// A price and confidence at a specific slot. This struct can represent either a 213 | /// publisher's contribution or the outcome of price aggregation. 214 | #[derive( 215 | Copy, 216 | Clone, 217 | Debug, 218 | Default, 219 | PartialEq, 220 | Eq, 221 | BorshSerialize, 222 | BorshDeserialize, 223 | serde::Serialize, 224 | serde::Deserialize, 225 | )] 226 | #[repr(C)] 227 | pub struct PriceInfo { 228 | /// the current price. 229 | /// For the aggregate price use `get_price_no_older_than()` whenever possible. Accessing fields 230 | /// directly might expose you to stale or invalid prices. 231 | pub price: i64, 232 | /// confidence interval around the price. 233 | /// For the aggregate confidence use `get_price_no_older_than()` whenever possible. Accessing 234 | /// fields directly might expose you to stale or invalid prices. 235 | pub conf: u64, 236 | /// status of price (Trading is valid) 237 | pub status: PriceStatus, 238 | /// notification of any corporate action 239 | pub corp_act: CorpAction, 240 | pub pub_slot: u64, 241 | } 242 | 243 | /// The price and confidence contributed by a specific publisher. 244 | #[derive( 245 | Copy, 246 | Clone, 247 | Debug, 248 | Default, 249 | PartialEq, 250 | Eq, 251 | BorshSerialize, 252 | BorshDeserialize, 253 | serde::Serialize, 254 | serde::Deserialize, 255 | )] 256 | #[repr(C)] 257 | pub struct PriceComp { 258 | /// key of contributing publisher 259 | pub publisher: Pubkey, 260 | /// the price used to compute the current aggregate price 261 | pub agg: PriceInfo, 262 | /// The publisher's latest price. This price will be incorporated into the aggregate price 263 | /// when price aggregation runs next. 264 | pub latest: PriceInfo, 265 | } 266 | 267 | #[deprecated = "Type is renamed to Rational, please use the new name."] 268 | pub type Ema = Rational; 269 | 270 | /// An number represented as both `value` and also in rational as `numer/denom`. 271 | #[derive( 272 | Copy, 273 | Clone, 274 | Debug, 275 | Default, 276 | PartialEq, 277 | Eq, 278 | BorshSerialize, 279 | BorshDeserialize, 280 | serde::Serialize, 281 | serde::Deserialize, 282 | )] 283 | #[repr(C)] 284 | pub struct Rational { 285 | pub val: i64, 286 | pub numer: i64, 287 | pub denom: i64, 288 | } 289 | 290 | #[repr(C)] 291 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 292 | pub struct GenericPriceAccount 293 | where 294 | T: Default, 295 | T: Copy, 296 | { 297 | /// pyth magic number 298 | pub magic: u32, 299 | /// program version 300 | pub ver: u32, 301 | /// account type 302 | pub atype: u32, 303 | /// price account size 304 | pub size: u32, 305 | /// price or calculation type 306 | pub ptype: PriceType, 307 | /// price exponent 308 | pub expo: i32, 309 | /// number of component prices 310 | pub num: u32, 311 | /// number of quoters that make up aggregate 312 | pub num_qt: u32, 313 | /// slot of last valid (not unknown) aggregate price 314 | pub last_slot: u64, 315 | /// valid slot-time of agg. price 316 | pub valid_slot: u64, 317 | /// exponentially moving average price 318 | pub ema_price: Rational, 319 | /// exponentially moving average confidence interval 320 | pub ema_conf: Rational, 321 | /// unix timestamp of aggregate price 322 | pub timestamp: i64, 323 | /// min publishers for valid price 324 | pub min_pub: u8, 325 | /// space for future derived values 326 | pub drv2: u8, 327 | /// space for future derived values 328 | pub drv3: u16, 329 | /// space for future derived values 330 | pub drv4: u32, 331 | /// product account key 332 | pub prod: Pubkey, 333 | /// next Price account in linked list 334 | pub next: Pubkey, 335 | /// valid slot of previous update 336 | pub prev_slot: u64, 337 | /// aggregate price of previous update with TRADING status 338 | pub prev_price: i64, 339 | /// confidence interval of previous update with TRADING status 340 | pub prev_conf: u64, 341 | /// unix timestamp of previous aggregate with TRADING status 342 | pub prev_timestamp: i64, 343 | /// aggregate price info 344 | pub agg: PriceInfo, 345 | /// price components one per quoter 346 | pub comp: [PriceComp; N], 347 | /// additional extended account data 348 | pub extended: T, 349 | } 350 | 351 | impl Default for GenericPriceAccount 352 | where 353 | T: Default, 354 | T: Copy, 355 | { 356 | fn default() -> Self { 357 | Self { 358 | magic: Default::default(), 359 | ver: Default::default(), 360 | atype: Default::default(), 361 | size: Default::default(), 362 | ptype: Default::default(), 363 | expo: Default::default(), 364 | num: Default::default(), 365 | num_qt: Default::default(), 366 | last_slot: Default::default(), 367 | valid_slot: Default::default(), 368 | ema_price: Default::default(), 369 | ema_conf: Default::default(), 370 | timestamp: Default::default(), 371 | min_pub: Default::default(), 372 | drv2: Default::default(), 373 | drv3: Default::default(), 374 | drv4: Default::default(), 375 | prod: Default::default(), 376 | next: Default::default(), 377 | prev_slot: Default::default(), 378 | prev_price: Default::default(), 379 | prev_conf: Default::default(), 380 | prev_timestamp: Default::default(), 381 | agg: Default::default(), 382 | comp: [Default::default(); N], 383 | extended: Default::default(), 384 | } 385 | } 386 | } 387 | 388 | impl std::ops::Deref for GenericPriceAccount 389 | where 390 | T: Default, 391 | T: Copy, 392 | { 393 | type Target = T; 394 | fn deref(&self) -> &Self::Target { 395 | &self.extended 396 | } 397 | } 398 | 399 | #[repr(C)] 400 | #[derive(Copy, Clone, Debug, Default, Pod, Zeroable, PartialEq, Eq)] 401 | pub struct PriceCumulative { 402 | /// Cumulative sum of price * slot_gap 403 | pub price: i128, 404 | /// Cumulative sum of conf * slot_gap 405 | pub conf: u128, 406 | /// Cumulative number of slots where the price wasn't recently updated (within 407 | /// PC_MAX_SEND_LATENCY slots). This field should be used to calculate the downtime 408 | /// as a percent of slots between two times `T` and `t` as follows: 409 | /// `(T.num_down_slots - t.num_down_slots) / (T.agg_.pub_slot_ - t.agg_.pub_slot_)` 410 | pub num_down_slots: u64, 411 | /// Padding for alignment 412 | pub unused: u64, 413 | } 414 | 415 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 416 | pub struct PriceAccountExt { 417 | pub price_cumulative: PriceCumulative, 418 | } 419 | 420 | /// Backwards compatibility. 421 | #[deprecated(note = "use an explicit SolanaPriceAccount or PythnetPriceAccount to avoid ambiguity")] 422 | pub type PriceAccount = GenericPriceAccount<32, ()>; 423 | 424 | /// Solana-specific Pyth account where the old 32-element publishers are present. 425 | pub type SolanaPriceAccount = GenericPriceAccount<32, ()>; 426 | 427 | /// Pythnet-specific Price accountw ith upgraded 64-element publishers and extended fields. 428 | pub type PythnetPriceAccount = GenericPriceAccount<128, PriceAccountExt>; 429 | 430 | #[cfg(target_endian = "little")] 431 | unsafe impl Zeroable for GenericPriceAccount { 432 | } 433 | 434 | #[cfg(target_endian = "little")] 435 | unsafe impl Pod for GenericPriceAccount { 436 | } 437 | 438 | impl GenericPriceAccount 439 | where 440 | T: Default, 441 | T: Copy, 442 | { 443 | pub fn get_publish_time(&self) -> UnixTimestamp { 444 | match self.agg.status { 445 | PriceStatus::Trading => self.timestamp, 446 | _ => self.prev_timestamp, 447 | } 448 | } 449 | 450 | /// Get the last valid price as long as it was updated within `slot_threshold` slots of the 451 | /// current slot. 452 | pub fn get_price_no_older_than(&self, clock: &Clock, slot_threshold: u64) -> Option { 453 | if self.agg.status == PriceStatus::Trading 454 | && self.agg.pub_slot >= clock.slot - slot_threshold 455 | { 456 | return Some(Price { 457 | conf: self.agg.conf, 458 | expo: self.expo, 459 | price: self.agg.price, 460 | publish_time: self.timestamp, 461 | }); 462 | } 463 | 464 | if self.prev_slot >= clock.slot - slot_threshold { 465 | return Some(Price { 466 | conf: self.prev_conf, 467 | expo: self.expo, 468 | price: self.prev_price, 469 | publish_time: self.prev_timestamp, 470 | }); 471 | } 472 | 473 | None 474 | } 475 | 476 | pub fn to_price_feed(&self, price_key: &Pubkey) -> PriceFeed { 477 | let status = self.agg.status; 478 | 479 | let price = match status { 480 | PriceStatus::Trading => Price { 481 | conf: self.agg.conf, 482 | expo: self.expo, 483 | price: self.agg.price, 484 | publish_time: self.get_publish_time(), 485 | }, 486 | _ => Price { 487 | conf: self.prev_conf, 488 | expo: self.expo, 489 | price: self.prev_price, 490 | publish_time: self.get_publish_time(), 491 | }, 492 | }; 493 | 494 | let ema_price = Price { 495 | conf: self.ema_conf.val as u64, 496 | expo: self.expo, 497 | price: self.ema_price.val, 498 | publish_time: self.get_publish_time(), 499 | }; 500 | 501 | PriceFeed::new(PriceIdentifier::new(price_key.to_bytes()), price, ema_price) 502 | } 503 | } 504 | 505 | fn load(data: &[u8]) -> Result<&T, PodCastError> { 506 | let size = size_of::(); 507 | if data.len() >= size { 508 | Ok(from_bytes(cast_slice::(try_cast_slice( 509 | &data[0..size], 510 | )?))) 511 | } else { 512 | Err(PodCastError::SizeMismatch) 513 | } 514 | } 515 | 516 | /// Get a `Mapping` account from the raw byte value of a Solana account. 517 | pub fn load_mapping_account(data: &[u8]) -> Result<&MappingAccount, PythError> { 518 | let pyth_mapping = load::(data).map_err(|_| PythError::InvalidAccountData)?; 519 | 520 | if pyth_mapping.magic != MAGIC { 521 | return Err(PythError::InvalidAccountData); 522 | } 523 | if pyth_mapping.ver != VERSION_2 { 524 | return Err(PythError::BadVersionNumber); 525 | } 526 | if pyth_mapping.atype != AccountType::Mapping as u32 { 527 | return Err(PythError::WrongAccountType); 528 | } 529 | 530 | Ok(pyth_mapping) 531 | } 532 | 533 | /// Get a `Product` account from the raw byte value of a Solana account. 534 | pub fn load_product_account(data: &[u8]) -> Result<&ProductAccount, PythError> { 535 | let pyth_product = load::(data).map_err(|_| PythError::InvalidAccountData)?; 536 | 537 | if pyth_product.magic != MAGIC { 538 | return Err(PythError::InvalidAccountData); 539 | } 540 | if pyth_product.ver != VERSION_2 { 541 | return Err(PythError::BadVersionNumber); 542 | } 543 | if pyth_product.atype != AccountType::Product as u32 { 544 | return Err(PythError::WrongAccountType); 545 | } 546 | 547 | Ok(pyth_product) 548 | } 549 | 550 | /// Get a `Price` account from the raw byte value of a Solana account. 551 | pub fn load_price_account( 552 | data: &[u8], 553 | ) -> Result<&GenericPriceAccount, PythError> { 554 | let pyth_price = 555 | load::>(data).map_err(|_| PythError::InvalidAccountData)?; 556 | 557 | if pyth_price.magic != MAGIC { 558 | return Err(PythError::InvalidAccountData); 559 | } 560 | if pyth_price.ver != VERSION_2 { 561 | return Err(PythError::BadVersionNumber); 562 | } 563 | if pyth_price.atype != AccountType::Price as u32 { 564 | return Err(PythError::WrongAccountType); 565 | } 566 | 567 | Ok(pyth_price) 568 | } 569 | 570 | pub struct AttributeIter<'a> { 571 | attrs: &'a [u8], 572 | } 573 | 574 | impl<'a> Iterator for AttributeIter<'a> { 575 | type Item = (&'a str, &'a str); 576 | 577 | fn next(&mut self) -> Option { 578 | if self.attrs.is_empty() { 579 | return None; 580 | } 581 | let (key, data) = get_attr_str(self.attrs)?; 582 | let (val, data) = get_attr_str(data)?; 583 | self.attrs = data; 584 | Some((key, val)) 585 | } 586 | } 587 | 588 | fn get_attr_str(buf: &[u8]) -> Option<(&str, &[u8])> { 589 | if buf.is_empty() { 590 | return Some(("", &[])); 591 | } 592 | let len = buf[0] as usize; 593 | let str = std::str::from_utf8(buf.get(1..len + 1)?).ok()?; 594 | let remaining_buf = &buf.get(len + 1..)?; 595 | Some((str, remaining_buf)) 596 | } 597 | 598 | #[cfg(test)] 599 | mod test { 600 | use pyth_sdk::{ 601 | Identifier, 602 | Price, 603 | PriceFeed, 604 | }; 605 | use solana_program::clock::Clock; 606 | use solana_program::pubkey::Pubkey; 607 | 608 | use crate::state::{ 609 | PROD_ACCT_SIZE, 610 | PROD_HDR_SIZE, 611 | }; 612 | 613 | use super::{ 614 | PriceInfo, 615 | PriceStatus, 616 | Rational, 617 | SolanaPriceAccount, 618 | }; 619 | 620 | #[test] 621 | fn test_trading_price_to_price_feed() { 622 | let price_account = SolanaPriceAccount { 623 | expo: 5, 624 | agg: PriceInfo { 625 | price: 10, 626 | conf: 20, 627 | status: PriceStatus::Trading, 628 | ..Default::default() 629 | }, 630 | timestamp: 200, 631 | prev_timestamp: 100, 632 | ema_price: Rational { 633 | val: 40, 634 | ..Default::default() 635 | }, 636 | ema_conf: Rational { 637 | val: 50, 638 | ..Default::default() 639 | }, 640 | prev_price: 60, 641 | prev_conf: 70, 642 | ..Default::default() 643 | }; 644 | 645 | let pubkey = Pubkey::new_from_array([3; 32]); 646 | let price_feed = price_account.to_price_feed(&pubkey); 647 | 648 | assert_eq!( 649 | price_feed, 650 | PriceFeed::new( 651 | Identifier::new(pubkey.to_bytes()), 652 | Price { 653 | conf: 20, 654 | price: 10, 655 | expo: 5, 656 | publish_time: 200, 657 | }, 658 | Price { 659 | conf: 50, 660 | price: 40, 661 | expo: 5, 662 | publish_time: 200, 663 | } 664 | ) 665 | ); 666 | } 667 | 668 | #[test] 669 | fn test_non_trading_price_to_price_feed() { 670 | let price_account = SolanaPriceAccount { 671 | expo: 5, 672 | agg: PriceInfo { 673 | price: 10, 674 | conf: 20, 675 | status: PriceStatus::Unknown, 676 | ..Default::default() 677 | }, 678 | timestamp: 200, 679 | prev_timestamp: 100, 680 | ema_price: Rational { 681 | val: 40, 682 | ..Default::default() 683 | }, 684 | ema_conf: Rational { 685 | val: 50, 686 | ..Default::default() 687 | }, 688 | prev_price: 60, 689 | prev_conf: 70, 690 | ..Default::default() 691 | }; 692 | 693 | let pubkey = Pubkey::new_from_array([3; 32]); 694 | let price_feed = price_account.to_price_feed(&pubkey); 695 | 696 | assert_eq!( 697 | price_feed, 698 | PriceFeed::new( 699 | Identifier::new(pubkey.to_bytes()), 700 | Price { 701 | conf: 70, 702 | price: 60, 703 | expo: 5, 704 | publish_time: 100, 705 | }, 706 | Price { 707 | conf: 50, 708 | price: 40, 709 | expo: 5, 710 | publish_time: 100, 711 | } 712 | ) 713 | ); 714 | } 715 | 716 | #[test] 717 | fn test_happy_use_latest_price_in_price_no_older_than() { 718 | let price_account = SolanaPriceAccount { 719 | expo: 5, 720 | agg: PriceInfo { 721 | price: 10, 722 | conf: 20, 723 | status: PriceStatus::Trading, 724 | pub_slot: 1, 725 | ..Default::default() 726 | }, 727 | timestamp: 200, 728 | prev_timestamp: 100, 729 | prev_price: 60, 730 | prev_conf: 70, 731 | ..Default::default() 732 | }; 733 | 734 | let clock = Clock { 735 | slot: 5, 736 | ..Default::default() 737 | }; 738 | 739 | assert_eq!( 740 | price_account.get_price_no_older_than(&clock, 4), 741 | Some(Price { 742 | conf: 20, 743 | expo: 5, 744 | price: 10, 745 | publish_time: 200, 746 | }) 747 | ); 748 | } 749 | 750 | #[test] 751 | fn test_happy_use_prev_price_in_price_no_older_than() { 752 | let price_account = SolanaPriceAccount { 753 | expo: 5, 754 | agg: PriceInfo { 755 | price: 10, 756 | conf: 20, 757 | status: PriceStatus::Unknown, 758 | pub_slot: 3, 759 | ..Default::default() 760 | }, 761 | timestamp: 200, 762 | prev_timestamp: 100, 763 | prev_price: 60, 764 | prev_conf: 70, 765 | prev_slot: 1, 766 | ..Default::default() 767 | }; 768 | 769 | let clock = Clock { 770 | slot: 5, 771 | ..Default::default() 772 | }; 773 | 774 | assert_eq!( 775 | price_account.get_price_no_older_than(&clock, 4), 776 | Some(Price { 777 | conf: 70, 778 | expo: 5, 779 | price: 60, 780 | publish_time: 100, 781 | }) 782 | ); 783 | } 784 | 785 | #[test] 786 | fn test_sad_cur_price_unknown_in_price_no_older_than() { 787 | let price_account = SolanaPriceAccount { 788 | expo: 5, 789 | agg: PriceInfo { 790 | price: 10, 791 | conf: 20, 792 | status: PriceStatus::Unknown, 793 | pub_slot: 3, 794 | ..Default::default() 795 | }, 796 | timestamp: 200, 797 | prev_timestamp: 100, 798 | prev_price: 60, 799 | prev_conf: 70, 800 | prev_slot: 1, 801 | ..Default::default() 802 | }; 803 | 804 | let clock = Clock { 805 | slot: 5, 806 | ..Default::default() 807 | }; 808 | 809 | // current price is unknown, prev price is too stale 810 | assert_eq!(price_account.get_price_no_older_than(&clock, 3), None); 811 | } 812 | 813 | #[test] 814 | fn test_sad_cur_price_stale_in_price_no_older_than() { 815 | let price_account = SolanaPriceAccount { 816 | expo: 5, 817 | agg: PriceInfo { 818 | price: 10, 819 | conf: 20, 820 | status: PriceStatus::Trading, 821 | pub_slot: 3, 822 | ..Default::default() 823 | }, 824 | timestamp: 200, 825 | prev_timestamp: 100, 826 | prev_price: 60, 827 | prev_conf: 70, 828 | prev_slot: 1, 829 | ..Default::default() 830 | }; 831 | 832 | let clock = Clock { 833 | slot: 5, 834 | ..Default::default() 835 | }; 836 | 837 | assert_eq!(price_account.get_price_no_older_than(&clock, 1), None); 838 | } 839 | 840 | #[test] 841 | fn test_price_feed_representations_equal() { 842 | #[repr(C)] 843 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 844 | pub struct OldPriceAccount { 845 | pub magic: u32, 846 | pub ver: u32, 847 | pub atype: u32, 848 | pub size: u32, 849 | pub ptype: crate::state::PriceType, 850 | pub expo: i32, 851 | pub num: u32, 852 | pub num_qt: u32, 853 | pub last_slot: u64, 854 | pub valid_slot: u64, 855 | pub ema_price: Rational, 856 | pub ema_conf: Rational, 857 | pub timestamp: i64, 858 | pub min_pub: u8, 859 | pub drv2: u8, 860 | pub drv3: u16, 861 | pub drv4: u32, 862 | pub prod: Pubkey, 863 | pub next: Pubkey, 864 | pub prev_slot: u64, 865 | pub prev_price: i64, 866 | pub prev_conf: u64, 867 | pub prev_timestamp: i64, 868 | pub agg: PriceInfo, 869 | pub comp: [crate::state::PriceComp; 32], 870 | } 871 | 872 | // Would be better to fuzz this but better than no check. 873 | let old = OldPriceAccount { 874 | magic: 1, 875 | ver: 2, 876 | atype: 3, 877 | size: 4, 878 | ptype: crate::state::PriceType::Price, 879 | expo: 5, 880 | num: 6, 881 | num_qt: 7, 882 | last_slot: 8, 883 | valid_slot: 9, 884 | ema_price: Rational { 885 | val: 1, 886 | numer: 2, 887 | denom: 3, 888 | }, 889 | ema_conf: Rational { 890 | val: 1, 891 | numer: 2, 892 | denom: 3, 893 | }, 894 | timestamp: 12, 895 | min_pub: 13, 896 | drv2: 14, 897 | drv3: 15, 898 | drv4: 16, 899 | prod: Pubkey::new_from_array([1; 32]), 900 | next: Pubkey::new_from_array([2; 32]), 901 | prev_slot: 19, 902 | prev_price: 20, 903 | prev_conf: 21, 904 | prev_timestamp: 22, 905 | agg: PriceInfo { 906 | price: 1, 907 | conf: 2, 908 | status: PriceStatus::Trading, 909 | corp_act: crate::state::CorpAction::NoCorpAct, 910 | pub_slot: 5, 911 | }, 912 | comp: [Default::default(); 32], 913 | }; 914 | 915 | let new = super::SolanaPriceAccount { 916 | magic: 1, 917 | ver: 2, 918 | atype: 3, 919 | size: 4, 920 | ptype: crate::state::PriceType::Price, 921 | expo: 5, 922 | num: 6, 923 | num_qt: 7, 924 | last_slot: 8, 925 | valid_slot: 9, 926 | ema_price: Rational { 927 | val: 1, 928 | numer: 2, 929 | denom: 3, 930 | }, 931 | ema_conf: Rational { 932 | val: 1, 933 | numer: 2, 934 | denom: 3, 935 | }, 936 | timestamp: 12, 937 | min_pub: 13, 938 | drv2: 14, 939 | drv3: 15, 940 | drv4: 16, 941 | prod: Pubkey::new_from_array([1; 32]), 942 | next: Pubkey::new_from_array([2; 32]), 943 | prev_slot: 19, 944 | prev_price: 20, 945 | prev_conf: 21, 946 | prev_timestamp: 22, 947 | agg: PriceInfo { 948 | price: 1, 949 | conf: 2, 950 | status: PriceStatus::Trading, 951 | corp_act: crate::state::CorpAction::NoCorpAct, 952 | pub_slot: 5, 953 | }, 954 | comp: [Default::default(); 32], 955 | extended: (), 956 | }; 957 | 958 | // Equal Sized? 959 | assert_eq!( 960 | std::mem::size_of::(), 961 | std::mem::size_of::(), 962 | ); 963 | 964 | // Equal Byte Representation? 965 | unsafe { 966 | let old_b = std::slice::from_raw_parts( 967 | &old as *const OldPriceAccount as *const u8, 968 | std::mem::size_of::(), 969 | ); 970 | let new_b = std::slice::from_raw_parts( 971 | &new as *const super::SolanaPriceAccount as *const u8, 972 | std::mem::size_of::(), 973 | ); 974 | assert_eq!(old_b, new_b); 975 | } 976 | } 977 | 978 | #[test] 979 | fn test_product_account_iter_works() { 980 | let mut product = super::ProductAccount { 981 | magic: 1, 982 | ver: 2, 983 | atype: super::AccountType::Product as u32, 984 | size: PROD_HDR_SIZE as u32 + 10, 985 | px_acc: Pubkey::new_from_array([3; 32]), 986 | attr: [0; super::PROD_ATTR_SIZE], 987 | }; 988 | 989 | // Set some attributes 990 | product.attr[0] = 3; // key length 991 | product.attr[1..4].copy_from_slice(b"key"); 992 | product.attr[4] = 5; // value length 993 | product.attr[5..10].copy_from_slice(b"value"); 994 | 995 | let mut iter = product.iter(); 996 | assert_eq!(iter.next(), Some(("key", "value"))); 997 | assert_eq!(iter.next(), None); 998 | 999 | // Check that the iterator does not panic on size misconfiguration 1000 | product.size = PROD_HDR_SIZE as u32 - 10; // Invalid size 1001 | let mut iter = product.iter(); 1002 | assert_eq!(iter.next(), None); // Should not panic, just return None 1003 | 1004 | product.size = PROD_ACCT_SIZE as u32 + 10; // Reset size to a size larger than the account size 1005 | let mut iter = product.iter(); 1006 | assert_eq!(iter.next(), Some(("key", "value"))); 1007 | while iter.next().is_some() {} // Consume the iterator 1008 | 1009 | // Check that invalid len stops the iterator. This behaviour is not perfect as it 1010 | // stops reading attributes after the first invalid one but is just a safety measure. 1011 | // In this case, we set the length byte to 255 which goes beyond the size of the 1012 | // product account. 1013 | product.attr[10] = 255; 1014 | for i in 11..266 { 1015 | product.attr[i] = b'a'; 1016 | } 1017 | product.attr[266] = 255; 1018 | for i in 267..super::PROD_ATTR_SIZE { 1019 | product.attr[i] = b'b'; 1020 | } 1021 | let mut iter = product.iter(); 1022 | assert_eq!(iter.next(), Some(("key", "value"))); 1023 | assert_eq!(iter.next(), None); // No more attributes because it stopped reading the invalid value 1024 | 1025 | // Make sure if the value size was set to a smaller value, it would work fine 1026 | product.attr[266] = 10; 1027 | let mut iter = product.iter(); 1028 | assert_eq!(iter.next(), Some(("key", "value"))); 1029 | let (key, val) = iter.next().unwrap(); 1030 | assert_eq!(key.len(), 255); 1031 | for byte in key.as_bytes() { 1032 | assert_eq!(byte, &b'a'); 1033 | } 1034 | assert_eq!(val, "bbbbbbbbbb"); // No more attributes because it stopped reading the invalid value 1035 | 1036 | // Check that iterator stops on non-UTF8 attributes. This behaviour is not 1037 | // perfect as it stops reading attributes after the first non-UTF8 one but 1038 | // is just a safety measure. 1039 | product.attr[1..4].copy_from_slice(b"\xff\xfe\xfa"); 1040 | let mut iter = product.iter(); 1041 | assert_eq!(iter.next(), None); // Should not panic, just return None 1042 | } 1043 | } 1044 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-contract" 3 | version = "0.3.0" 4 | workspace = "../../" 5 | edition = "2018" 6 | 7 | [features] 8 | test-bpf = [] 9 | no-entrypoint = [] 10 | 11 | [dependencies] 12 | pyth-sdk-solana = { path = "../", version = "0.10.0" } 13 | solana-program = ">= 1.10" 14 | bytemuck = "1.7.2" 15 | borsh = "0.10.3" 16 | borsh-derive = "0.10.3" 17 | 18 | [dev-dependencies] 19 | solana-program-test = ">= 1.10" 20 | solana-client = ">= 1.10" 21 | solana-sdk = ">= 1.10" 22 | 23 | [lib] 24 | crate-type = ["cdylib", "lib"] 25 | 26 | [package.metadata.docs.rs] 27 | targets = ["x86_64-unknown-linux-gnu"] 28 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/README.md: -------------------------------------------------------------------------------- 1 | # Pyth SDK Solana Test Contract 2 | This contract is used to test pyth-sdk-solana onchain. 3 | 4 | ## Development 5 | 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). 6 | 7 | The BPF tests will also run an instruction count program that logs the resource consumption 8 | of various library functions. 9 | This program can also be run on its own using `cargo test-bpf --test instruction_count`. 10 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/rust-toolchain: -------------------------------------------------------------------------------- 1 | 2 | # This is only used for tests 3 | [toolchain] 4 | channel = "1.76.0" 5 | profile = "minimal" 6 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | //! Program entrypoint 2 | 3 | #![cfg(not(feature = "no-entrypoint"))] 4 | 5 | use solana_program::account_info::AccountInfo; 6 | use solana_program::entrypoint; 7 | use solana_program::entrypoint::ProgramResult; 8 | use solana_program::pubkey::Pubkey; 9 | 10 | entrypoint!(process_instruction); 11 | fn process_instruction( 12 | program_id: &Pubkey, 13 | accounts: &[AccountInfo], 14 | instruction_data: &[u8], 15 | ) -> ProgramResult { 16 | crate::processor::process_instruction(program_id, accounts, instruction_data) 17 | } 18 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/src/instruction.rs: -------------------------------------------------------------------------------- 1 | //! Program instructions for end-to-end testing and instruction counts 2 | 3 | use pyth_sdk_solana::Price; 4 | 5 | use crate::id; 6 | use borsh::{ 7 | BorshDeserialize, 8 | BorshSerialize, 9 | }; 10 | use solana_program::instruction::Instruction; 11 | 12 | /// Instructions supported by the pyth-client program, used for testing and 13 | /// instruction counts 14 | #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] 15 | pub enum PythClientInstruction { 16 | Divide { 17 | numerator: Price, 18 | denominator: Price, 19 | }, 20 | Multiply { 21 | x: Price, 22 | y: Price, 23 | }, 24 | Add { 25 | x: Price, 26 | y: Price, 27 | }, 28 | ScaleToExponent { 29 | x: Price, 30 | expo: i32, 31 | }, 32 | Normalize { 33 | x: Price, 34 | }, 35 | /// Don't do anything for comparison 36 | /// 37 | /// No accounts required for this instruction 38 | Noop, 39 | } 40 | 41 | pub fn divide(numerator: Price, denominator: Price) -> Instruction { 42 | Instruction { 43 | program_id: id(), 44 | accounts: vec![], 45 | data: PythClientInstruction::Divide { 46 | numerator, 47 | denominator, 48 | } 49 | .try_to_vec() 50 | .unwrap(), 51 | } 52 | } 53 | 54 | pub fn multiply(x: Price, y: Price) -> Instruction { 55 | Instruction { 56 | program_id: id(), 57 | accounts: vec![], 58 | data: PythClientInstruction::Multiply { x, y } 59 | .try_to_vec() 60 | .unwrap(), 61 | } 62 | } 63 | 64 | pub fn add(x: Price, y: Price) -> Instruction { 65 | Instruction { 66 | program_id: id(), 67 | accounts: vec![], 68 | data: PythClientInstruction::Add { x, y }.try_to_vec().unwrap(), 69 | } 70 | } 71 | 72 | pub fn scale_to_exponent(x: Price, expo: i32) -> Instruction { 73 | Instruction { 74 | program_id: id(), 75 | accounts: vec![], 76 | data: PythClientInstruction::ScaleToExponent { x, expo } 77 | .try_to_vec() 78 | .unwrap(), 79 | } 80 | } 81 | 82 | pub fn normalize(x: Price) -> Instruction { 83 | Instruction { 84 | program_id: id(), 85 | accounts: vec![], 86 | data: PythClientInstruction::Normalize { x }.try_to_vec().unwrap(), 87 | } 88 | } 89 | 90 | /// Noop instruction for comparison purposes 91 | pub fn noop() -> Instruction { 92 | Instruction { 93 | program_id: id(), 94 | accounts: vec![], 95 | data: PythClientInstruction::Noop.try_to_vec().unwrap(), 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod entrypoint; 2 | pub mod instruction; 3 | pub mod processor; 4 | 5 | // This is used only in local testing. 6 | solana_program::declare_id!("PythC11111111111111111111111111111111111111"); 7 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/src/processor.rs: -------------------------------------------------------------------------------- 1 | //! Program instruction processor for end-to-end testing and instruction counts 2 | 3 | use borsh::BorshDeserialize; 4 | use solana_program::account_info::AccountInfo; 5 | use solana_program::entrypoint::ProgramResult; 6 | use solana_program::pubkey::Pubkey; 7 | 8 | use crate::instruction::PythClientInstruction; 9 | 10 | pub fn process_instruction( 11 | _program_id: &Pubkey, 12 | _accounts: &[AccountInfo], 13 | input: &[u8], 14 | ) -> ProgramResult { 15 | let instruction = PythClientInstruction::try_from_slice(input).unwrap(); 16 | match instruction { 17 | PythClientInstruction::Divide { 18 | numerator, 19 | denominator, 20 | } => { 21 | numerator.div(&denominator); 22 | Ok(()) 23 | } 24 | PythClientInstruction::Multiply { x, y } => { 25 | x.mul(&y); 26 | Ok(()) 27 | } 28 | PythClientInstruction::Add { x, y } => { 29 | x.add(&y); 30 | Ok(()) 31 | } 32 | PythClientInstruction::Normalize { x } => { 33 | x.normalize(); 34 | Ok(()) 35 | } 36 | PythClientInstruction::ScaleToExponent { x, expo } => { 37 | x.scale_to_exponent(expo); 38 | Ok(()) 39 | } 40 | PythClientInstruction::Noop => Ok(()), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/tests/common.rs: -------------------------------------------------------------------------------- 1 | use solana_program::instruction::Instruction; 2 | use solana_program_test::*; 3 | use solana_sdk::signature::Signer; 4 | use solana_sdk::transaction::Transaction; 5 | use test_contract::id; 6 | use test_contract::processor::process_instruction; 7 | 8 | // Panics if running instruction fails 9 | pub async fn test_instr_exec_ok(instr: Instruction) { 10 | let mut context = ProgramTest::new("test_contract", id(), processor!(process_instruction)) 11 | .start_with_context() 12 | .await; 13 | 14 | context.warp_to_slot(1000).unwrap(); 15 | 16 | let mut transaction = Transaction::new_with_payer(&[instr], Some(&context.payer.pubkey())); 17 | transaction.sign(&[&context.payer], context.last_blockhash); 18 | context 19 | .banks_client 20 | .process_transaction(transaction) 21 | .await 22 | .unwrap() 23 | } 24 | -------------------------------------------------------------------------------- /pyth-sdk-solana/test-contract/tests/instruction_count.rs: -------------------------------------------------------------------------------- 1 | use solana_program_test::*; 2 | use test_contract::instruction; 3 | 4 | use pyth_sdk_solana::Price; 5 | 6 | mod common; 7 | use common::test_instr_exec_ok; 8 | 9 | fn pc(price: i64, conf: u64, expo: i32) -> Price { 10 | Price { 11 | price, 12 | conf, 13 | expo, 14 | publish_time: 0, 15 | } 16 | } 17 | 18 | #[tokio::test] 19 | async fn test_noop() { 20 | test_instr_exec_ok(instruction::noop()).await; 21 | } 22 | 23 | #[tokio::test] 24 | async fn test_scale_to_exponent_down() { 25 | test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, -1000), 1000)).await 26 | } 27 | 28 | #[tokio::test] 29 | async fn test_scale_to_exponent_up() { 30 | test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, 1000), -1000)).await 31 | } 32 | 33 | #[tokio::test] 34 | async fn test_scale_to_exponent_best_case() { 35 | test_instr_exec_ok(instruction::scale_to_exponent(pc(1, u64::MAX, 10), 10)).await 36 | } 37 | 38 | #[tokio::test] 39 | async fn test_normalize_max_conf() { 40 | test_instr_exec_ok(instruction::normalize(pc(1, u64::MAX, 0))).await 41 | } 42 | 43 | #[tokio::test] 44 | async fn test_normalize_max_price() { 45 | test_instr_exec_ok(instruction::normalize(pc(i64::MAX, 1, 0))).await 46 | } 47 | 48 | #[tokio::test] 49 | async fn test_normalize_min_price() { 50 | test_instr_exec_ok(instruction::normalize(pc(i64::MIN, 1, 0))).await 51 | } 52 | 53 | #[tokio::test] 54 | async fn test_normalize_best_case() { 55 | test_instr_exec_ok(instruction::normalize(pc(1, 1, 0))).await 56 | } 57 | 58 | #[tokio::test] 59 | async fn test_div_max_price() { 60 | test_instr_exec_ok(instruction::divide(pc(i64::MAX, 1, 0), pc(1, 1, 0))).await; 61 | } 62 | 63 | #[tokio::test] 64 | async fn test_div_max_price_2() { 65 | test_instr_exec_ok(instruction::divide(pc(i64::MAX, 1, 0), pc(i64::MAX, 1, 0))).await; 66 | } 67 | 68 | #[tokio::test] 69 | async fn test_mul_max_price() { 70 | test_instr_exec_ok(instruction::multiply(pc(i64::MAX, 1, 2), pc(123, 1, 2))).await; 71 | } 72 | 73 | #[tokio::test] 74 | async fn test_mul_max_price_2() { 75 | test_instr_exec_ok(instruction::multiply( 76 | pc(i64::MAX, 1, 2), 77 | pc(i64::MAX, 1, 2), 78 | )) 79 | .await; 80 | } 81 | -------------------------------------------------------------------------------- /pyth-sdk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyth-sdk" 3 | version = "0.8.0" 4 | authors = ["Pyth Data Foundation"] 5 | workspace = "../" 6 | edition = "2018" 7 | license = "Apache-2.0" 8 | homepage = "https://pyth.network" 9 | repository = "https://github.com/pyth-network/pyth-sdk-rs" 10 | description = "Data structures and utilites for the Pyth price oracle" 11 | keywords = [ "pyth", "oracle" ] 12 | readme = "README.md" 13 | 14 | [lib] 15 | crate-type = ["cdylib", "lib"] 16 | 17 | [dependencies] 18 | hex = { version = "0.4.3", features = ["serde"] } 19 | borsh.workspace = true 20 | borsh-derive.workspace = true 21 | serde = { workspace = true, features = ["derive"] } 22 | schemars = "0.8.8" 23 | getrandom = { version = "0.2.2", features = ["custom"] } 24 | 25 | [dev-dependencies] 26 | serde_json = "1.0.79" 27 | quickcheck = "1" 28 | quickcheck_macros = "1" 29 | -------------------------------------------------------------------------------- /pyth-sdk/README.md: -------------------------------------------------------------------------------- 1 | # Pyth Network Common Rust SDK 2 | 3 | This crate contains Pyth Network data structures that are shared across all Rust-based consumers of Pyth Network data. 4 | This crate is typically used in combination with a platform-specific crate such as [pyth-sdk-solana](../pyth-sdk-solana). 5 | 6 | ## Usage 7 | 8 | The SDK has two core data types: 9 | 10 | * `PriceFeed` is a container for all currently-available pricing information about a product (e.g., BTC/USD). 11 | * `Price` represents a price with a degree of uncertainty. 12 | 13 | The typical usage of this SDK is to first retrieve a `PriceFeed` for one or more products required by your application. 14 | This step typically uses one of the platform-specific crates (referenced above), which provide retrieval methods for specific blockchains. 15 | Once you have a `PriceFeed`, you can call one of the methods below to get the prices your application needs: 16 | 17 | ### Get the Current Price 18 | 19 | Get the current price of the product from its `PriceFeed`: 20 | 21 | ```rust 22 | const STALENESS_THRESHOLD : u64 = 60; // staleness threshold in seconds 23 | let current_timestamp = ...; 24 | let current_price: Price = price_feed.get_price_no_older_than(current_timestamp, STALENESS_THRESHOLD).ok_or(StdError::not_found("Current price is not available"))?; 25 | println!("price: ({} +- {}) x 10^{}", current_price.price, current_price.conf, current_price.expo); 26 | ``` 27 | 28 | The price is returned along with a confidence interval that represents the degree of uncertainty in the price. 29 | Both values are represented as fixed-point numbers, `a * 10^e`. 30 | The method will return `None` if the current price is not available. 31 | 32 | Please see the [consumer best practices guide](https://docs.pyth.network/consumers/best-practices) for additional recommendations on how to consume Pyth Network prices, such as how to use the confidence interval, and what to do if the price is not currently available. 33 | 34 | ### EMA Price 35 | 36 | `PriceFeed` includes an exponentially-weighted moving average (EMA) price that represents a time-average of recent prices. 37 | The EMA price can be retrieved as follows: 38 | 39 | ```rust 40 | const STALENESS_THRESHOLD : u64 = 60; // staleness threshold in seconds 41 | let current_timestamp = ...; 42 | let ema_price: Price = price_feed.get_ema_price_no_older_than(current_timestamp, STALENESS_THRESHOLD).ok_or(StdError::not_found("EMA price is not available"))?; 43 | println!("price: ({} +- {}) x 10^{}", ema_price.price, ema_price.conf, ema_price.expo); 44 | ``` 45 | 46 | ## Manipulating Prices 47 | 48 | The `Price` struct supports arithmetic operations that allow you to combine prices from multiple products. 49 | These operations can be used to price some products that aren't directly listed on Pyth Network: 50 | 51 | ### Non-USD Prices 52 | 53 | Most assets listed on Pyth Network are quoted in terms of USD, e.g., the BTC/USD price feed provides the number of dollars per BTC. 54 | However, some applications would like prices in other quote currencies, such as the number of ETH per BTC. 55 | Applications can combine two USD prices to price an asset in a different quote currency: 56 | 57 | ```rust 58 | let btc_usd: Price = ...; 59 | let eth_usd: Price = ...; 60 | // -8 is the desired exponent for the result 61 | let btc_eth: Price = btc_usd.get_price_in_quote(ð_usd, -8); 62 | println!("BTC/ETH price: ({} +- {}) x 10^{}", price.price, price.conf, price.expo); 63 | ``` 64 | 65 | ### Price a Basket of Assets 66 | 67 | Applications can also compute the value of a basket of multiple assets: 68 | 69 | ```rust 70 | let btc_usd: Price = ...; 71 | let eth_usd: Price = ...; 72 | // Quantity of each asset in fixed-point a * 10^e. 73 | // This represents 0.1 BTC and .05 ETH. 74 | // -8 is desired exponent for result 75 | let basket_price: Price = Price::price_basket(&[ 76 | (btc_usd, 10, -2), 77 | (eth_usd, 5, -2) 78 | ], -8); 79 | println!("0.1 BTC and 0.05 ETH are worth: ({} +- {}) x 10^{} USD", 80 | basket_price.price, basket_price.conf, basket_price.expo); 81 | ``` 82 | 83 | This operation can be useful for pricing, e.g., an LP token that is backed by two underlying currencies. 84 | 85 | ## Adjust Prices using Liquidity 86 | 87 | Applications can adjust Pyth prices to incorporate market impact and liquidity for large positions, since the effective transaction price for large positions differs from the midprice and top-of-book price. Suppose the application wants to value the effective execution price on selling 100 BTC, based on the fact that 1000 BTC sell at a 10% discount (90% of the midprice). Based on assuming the market impact is constant with respect to amount sold, the application can combine the current Pyth price and its liquidity views to calculate the adjusted price: 88 | 89 | ```rust 90 | let btc_usd: Price = ...; 91 | // deposits is the total number of tokens deposited in the protocol 92 | let deposits: u64 = 100; 93 | // deposits_endpoint represents the token deposits at which rate_discount_final kicks in 94 | let deposits_endpoint: u64 = 1000; 95 | // rate_discount_initial and _final are percentages, expressed in fixed point as x * 10^discount_exponent. 96 | // E.g. 50 with discount_exponent = -2 is 50%. 97 | let rate_discount_initial: u64 = 100; 98 | let rate_discount_final: u64 = 90; 99 | let discount_exponent: i32 = 2; 100 | 101 | let price_collateral: Price = btc_usd.get_collateral_valuation_price( 102 | deposits, 103 | deposits_endpoint, 104 | rate_discount_initial, 105 | rate_discount_final, 106 | discount_exponent).ok_or(StdError::not_found("Issue with querying collateral price"))?; 107 | println!("The valuation price for the collateral given {} tokens deposited is ({} +- {}) x 10^{} USD", 108 | deposits, price_collateral.price, price_collateral.conf, price_collateral.expo); 109 | ``` 110 | 111 | Here, `deposits` indicates the total amount of collateral deposited. `get_collateral_valuation_price` takes in the total deposits in the protocol and linearly interpolates between (`0`, `rate_discount_inital`) and (`deposits_endpoint`, `rate_discount_final`) to calculate the discount at `deposits`. As a note, this function scales the price depending on the provided discount and deposit inputs, but it does not alter the confidence. 112 | 113 | To adjust the price at which a borrow position is valued, a protocol can similarly combine the current Pyth price and their estimate of liquidity, but using the `get_borrow_valuation_price` function now in place of `get_collateral_valuation_price`. 114 | -------------------------------------------------------------------------------- /pyth-sdk/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use schemars::schema_for; 2 | use serde_json::to_string_pretty; 3 | use std::env::current_dir; 4 | use std::fs::{ 5 | create_dir_all, 6 | write, 7 | }; 8 | 9 | use pyth_sdk::PriceFeed; 10 | 11 | fn main() { 12 | let mut out_dir = current_dir().unwrap(); 13 | out_dir.push("schema"); 14 | create_dir_all(&out_dir).unwrap(); 15 | 16 | let schema = &schema_for!(PriceFeed); 17 | let json = to_string_pretty(schema).unwrap(); 18 | let path = out_dir.join(format!("{}.json", "price_feed")); 19 | write(&path, json + "\n").unwrap(); 20 | println!("Updated {}", path.to_str().unwrap()); 21 | } 22 | -------------------------------------------------------------------------------- /pyth-sdk/schema/price_feed.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "PriceFeed", 4 | "description": "Represents a current aggregation price from pyth publisher feeds.", 5 | "type": "object", 6 | "required": [ 7 | "ema_price", 8 | "id", 9 | "price" 10 | ], 11 | "properties": { 12 | "ema_price": { 13 | "description": "Exponentially-weighted moving average (EMA) price.", 14 | "allOf": [ 15 | { 16 | "$ref": "#/definitions/Price" 17 | } 18 | ] 19 | }, 20 | "id": { 21 | "description": "Unique identifier for this price.", 22 | "allOf": [ 23 | { 24 | "$ref": "#/definitions/Identifier" 25 | } 26 | ] 27 | }, 28 | "price": { 29 | "description": "Price.", 30 | "allOf": [ 31 | { 32 | "$ref": "#/definitions/Price" 33 | } 34 | ] 35 | } 36 | }, 37 | "definitions": { 38 | "Identifier": { 39 | "type": "string" 40 | }, 41 | "Price": { 42 | "description": "A price with a degree of uncertainty at a certain time, represented as a price +- a confidence interval.\n\nPlease refer to the documentation at https://docs.pyth.network/consumers/best-practices for using this price safely.\n\nThe confidence interval roughly corresponds to the standard error of a normal distribution. Both the price and confidence are stored in a fixed-point numeric representation, `x * 10^expo`, where `expo` is the exponent. For example:\n\n``` use pyth_sdk::Price; Price { price: 12345, conf: 267, expo: -2, publish_time: 100 }; // represents 123.45 +- 2.67 published at UnixTimestamp 100 Price { price: 123, conf: 1, expo: 2, publish_time: 100 }; // represents 12300 +- 100 published at UnixTimestamp 100 ```\n\n`Price` supports a limited set of mathematical operations. All of these operations will propagate any uncertainty in the arguments into the result. However, the uncertainty in the result may overestimate the true uncertainty (by at most a factor of `sqrt(2)`) due to computational limitations. Furthermore, all of these operations may return `None` if their result cannot be represented within the numeric representation (e.g., the exponent is so small that the price does not fit into an i64). Users of these methods should (1) select their exponents to avoid this problem, and (2) handle the `None` case gracefully.", 43 | "type": "object", 44 | "required": [ 45 | "conf", 46 | "expo", 47 | "price", 48 | "publish_time" 49 | ], 50 | "properties": { 51 | "conf": { 52 | "description": "Confidence interval.", 53 | "type": "string" 54 | }, 55 | "expo": { 56 | "description": "Exponent.", 57 | "type": "integer", 58 | "format": "int32" 59 | }, 60 | "price": { 61 | "description": "Price.", 62 | "type": "string" 63 | }, 64 | "publish_time": { 65 | "description": "Publish time.", 66 | "type": "integer", 67 | "format": "int64" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pyth-sdk/src/lib.rs: -------------------------------------------------------------------------------- 1 | use borsh::{ 2 | BorshDeserialize, 3 | BorshSerialize, 4 | }; 5 | 6 | use hex::FromHexError; 7 | use schemars::JsonSchema; 8 | use std::fmt; 9 | 10 | pub mod utils; 11 | 12 | mod price; 13 | pub use price::Price; 14 | 15 | #[derive( 16 | Copy, 17 | Clone, 18 | Default, 19 | PartialEq, 20 | Eq, 21 | PartialOrd, 22 | Ord, 23 | Hash, 24 | BorshSerialize, 25 | BorshDeserialize, 26 | serde::Serialize, 27 | serde::Deserialize, 28 | JsonSchema, 29 | )] 30 | #[repr(C)] 31 | pub struct Identifier( 32 | #[serde(with = "hex")] 33 | #[schemars(with = "String")] 34 | [u8; 32], 35 | ); 36 | 37 | impl Identifier { 38 | pub fn new(bytes: [u8; 32]) -> Identifier { 39 | Identifier(bytes) 40 | } 41 | 42 | pub fn to_bytes(&self) -> [u8; 32] { 43 | self.0 44 | } 45 | 46 | pub fn to_hex(&self) -> String { 47 | hex::encode(self.0) 48 | } 49 | 50 | pub fn from_hex>(s: T) -> Result { 51 | let mut bytes = [0u8; 32]; 52 | hex::decode_to_slice(s, &mut bytes)?; 53 | Ok(Identifier::new(bytes)) 54 | } 55 | } 56 | 57 | impl fmt::Debug for Identifier { 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 59 | write!(f, "0x{}", self.to_hex()) 60 | } 61 | } 62 | 63 | impl fmt::Display for Identifier { 64 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 65 | write!(f, "0x{}", self.to_hex()) 66 | } 67 | } 68 | 69 | impl AsRef<[u8]> for Identifier { 70 | fn as_ref(&self) -> &[u8] { 71 | &self.0[..] 72 | } 73 | } 74 | 75 | /// Consists of 32 bytes and it is currently based on largest Public Key size on various 76 | /// blockchains. 77 | pub type PriceIdentifier = Identifier; 78 | 79 | /// Consists of 32 bytes and it is currently based on largest Public Key size on various 80 | /// blockchains. 81 | pub type ProductIdentifier = Identifier; 82 | 83 | /// Unix Timestamp is represented as number of seconds passed since Unix epoch (00:00:00 UTC on 1 84 | /// Jan 1970). It is a signed integer because it's the standard in Unix systems and allows easier 85 | /// time difference. 86 | pub type UnixTimestamp = i64; 87 | pub type DurationInSeconds = u64; 88 | 89 | /// Represents a current aggregation price from pyth publisher feeds. 90 | #[derive( 91 | Copy, 92 | Clone, 93 | Debug, 94 | Default, 95 | PartialEq, 96 | Eq, 97 | BorshSerialize, 98 | BorshDeserialize, 99 | serde::Serialize, 100 | serde::Deserialize, 101 | JsonSchema, 102 | )] 103 | #[repr(C)] 104 | pub struct PriceFeed { 105 | /// Unique identifier for this price. 106 | pub id: PriceIdentifier, 107 | /// Price. 108 | price: Price, 109 | /// Exponentially-weighted moving average (EMA) price. 110 | ema_price: Price, 111 | } 112 | 113 | impl PriceFeed { 114 | /// Constructs a new Price Feed 115 | #[allow(clippy::too_many_arguments)] 116 | pub fn new(id: PriceIdentifier, price: Price, ema_price: Price) -> PriceFeed { 117 | PriceFeed { 118 | id, 119 | price, 120 | ema_price, 121 | } 122 | } 123 | 124 | 125 | /// Get the "unchecked" price and confidence interval as fixed-point numbers of the form 126 | /// a * 10^e along with its publish time. 127 | /// 128 | /// Returns a `Price` struct containing the current price, confidence interval, and the exponent 129 | /// for both numbers, and publish time. This method returns the latest price which may be from 130 | /// arbitrarily far in the past, and the caller should probably check the timestamp before using 131 | /// it. 132 | /// 133 | /// Please consider using `get_price_no_older_than` when possible. 134 | pub fn get_price_unchecked(&self) -> Price { 135 | self.price 136 | } 137 | 138 | 139 | /// Get the "unchecked" exponentially-weighted moving average (EMA) price and a confidence 140 | /// interval on the result along with its publish time. 141 | /// 142 | /// Returns the latest EMA price value which may be from arbitrarily far in the past, and the 143 | /// caller should probably check the timestamp before using it. 144 | /// 145 | /// At the moment, the confidence interval returned by this method is computed in 146 | /// a somewhat questionable way, so we do not recommend using it for high-value applications. 147 | /// 148 | /// Please consider using `get_ema_price_no_older_than` when possible. 149 | pub fn get_ema_price_unchecked(&self) -> Price { 150 | self.ema_price 151 | } 152 | 153 | /// Get the price as long as it was updated within `age` seconds of the 154 | /// `current_time`. 155 | /// 156 | /// This function is a sanity-checked version of `get_price_unchecked` which is 157 | /// useful in applications that require a sufficiently-recent price. Returns `None` if the 158 | /// price wasn't updated sufficiently recently. 159 | /// 160 | /// Returns a struct containing the latest available price, confidence interval and the exponent 161 | /// for both numbers, or `None` if no price update occurred within `age` seconds of the 162 | /// `current_time`. 163 | pub fn get_price_no_older_than( 164 | &self, 165 | current_time: UnixTimestamp, 166 | age: DurationInSeconds, 167 | ) -> Option { 168 | let price = self.get_price_unchecked(); 169 | 170 | let time_diff_abs = (price.publish_time - current_time).unsigned_abs(); 171 | 172 | if time_diff_abs > age { 173 | return None; 174 | } 175 | 176 | Some(price) 177 | } 178 | 179 | /// Get the exponentially-weighted moving average (EMA) price as long as it was updated within 180 | /// `age` seconds of the `current_time`. 181 | /// 182 | /// This function is a sanity-checked version of `get_ema_price_unchecked` which is useful in 183 | /// applications that require a sufficiently-recent price. Returns `None` if the price 184 | /// wasn't updated sufficiently recently. 185 | /// 186 | /// Returns a struct containing the EMA price, confidence interval and the exponent 187 | /// for both numbers, or `None` if no price update occurred within `age` seconds of the 188 | /// `current_time`. 189 | pub fn get_ema_price_no_older_than( 190 | &self, 191 | current_time: UnixTimestamp, 192 | age: DurationInSeconds, 193 | ) -> Option { 194 | let price = self.get_ema_price_unchecked(); 195 | 196 | let time_diff_abs = (price.publish_time - current_time).unsigned_abs(); 197 | 198 | if time_diff_abs > age { 199 | return None; 200 | } 201 | 202 | Some(price) 203 | } 204 | } 205 | #[cfg(test)] 206 | mod test { 207 | use super::*; 208 | 209 | #[test] 210 | pub fn test_ser_then_deser_default() { 211 | let price_feed = PriceFeed::default(); 212 | let ser = serde_json::to_string(&price_feed).unwrap(); 213 | let deser: PriceFeed = serde_json::from_str(&ser).unwrap(); 214 | assert_eq!(price_feed, deser); 215 | } 216 | 217 | #[test] 218 | pub fn test_ser_large_number() { 219 | let price_feed = PriceFeed { 220 | ema_price: Price { 221 | conf: 1_234_567_000_000_000_789, 222 | ..Price::default() 223 | }, 224 | ..PriceFeed::default() 225 | }; 226 | let price_feed_json = serde_json::to_value(price_feed).unwrap(); 227 | assert_eq!( 228 | price_feed_json["ema_price"]["conf"].as_str(), 229 | Some("1234567000000000789") 230 | ); 231 | } 232 | 233 | #[test] 234 | pub fn test_deser_large_number() { 235 | let mut price_feed_json = serde_json::to_value(PriceFeed::default()).unwrap(); 236 | price_feed_json["price"]["price"] = 237 | serde_json::Value::String(String::from("1000000000000000123")); 238 | let p: PriceFeed = serde_json::from_value(price_feed_json).unwrap(); 239 | assert_eq!(p.get_price_unchecked().price, 1_000_000_000_000_000_123); 240 | } 241 | 242 | #[test] 243 | pub fn test_ser_id_length_32_bytes() { 244 | let mut price_feed = PriceFeed::default(); 245 | price_feed.id.0[0] = 106; // 0x6a 246 | let price_feed_json = serde_json::to_value(price_feed).unwrap(); 247 | let id_str = price_feed_json["id"].as_str().unwrap(); 248 | assert_eq!(id_str.len(), 64); 249 | assert_eq!( 250 | id_str, 251 | "6a00000000000000000000000000000000000000000000000000000000000000" 252 | ); 253 | } 254 | 255 | #[test] 256 | pub fn test_deser_invalid_id_length_fails() { 257 | let mut price_feed_json = serde_json::to_value(PriceFeed::default()).unwrap(); 258 | price_feed_json["id"] = serde_json::Value::String(String::from("1234567890")); 259 | assert!(serde_json::from_value::(price_feed_json).is_err()); 260 | } 261 | 262 | #[test] 263 | pub fn test_identifier_from_hex_ok() { 264 | let id = Identifier::from_hex( 265 | "0a3f000000000000000000000000000000000000000000000000000000000000", 266 | ) 267 | .unwrap(); 268 | assert_eq!(id.to_bytes()[0], 10); 269 | } 270 | 271 | #[test] 272 | pub fn test_identifier_from_hex_invalid_err() { 273 | let try_parse_odd = Identifier::from_hex("010"); // odd length 274 | assert_eq!(try_parse_odd, Err(FromHexError::OddLength)); 275 | 276 | let try_parse_invalid_len = Identifier::from_hex("0a"); // length should be 32 bytes, 64 277 | assert_eq!( 278 | try_parse_invalid_len, 279 | Err(FromHexError::InvalidStringLength) 280 | ); 281 | } 282 | 283 | #[test] 284 | pub fn test_identifier_debug_fmt() { 285 | let mut id = Identifier::default(); 286 | id.0[0] = 10; 287 | 288 | let id_str = format!("{:?}", id); 289 | assert_eq!( 290 | id_str, 291 | "0x0a00000000000000000000000000000000000000000000000000000000000000" 292 | ); 293 | } 294 | 295 | #[test] 296 | pub fn test_identifier_display_fmt() { 297 | let mut id = Identifier::default(); 298 | id.0[0] = 10; 299 | 300 | let id_str = format!("{}", id); 301 | assert_eq!( 302 | id_str, 303 | "0x0a00000000000000000000000000000000000000000000000000000000000000" 304 | ); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /pyth-sdk/src/utils.rs: -------------------------------------------------------------------------------- 1 | /// This module helps serde to serialize deserialize some fields as String 2 | /// 3 | /// The reason this is added is that `#[serde(with = "String")]` does not work 4 | /// because Borsh also implements serialize and deserialize functions and 5 | /// compiler cannot distinguish them. 6 | pub mod as_string { 7 | use serde::de::Error; 8 | use serde::{ 9 | Deserialize, 10 | Deserializer, 11 | Serializer, 12 | }; 13 | 14 | pub fn serialize(value: &T, serializer: S) -> Result 15 | where 16 | T: std::fmt::Display, 17 | S: Serializer, 18 | { 19 | serializer.serialize_str(value.to_string().as_str()) 20 | } 21 | 22 | pub fn deserialize<'de, T, D>(deserializer: D) -> Result 23 | where 24 | T: std::str::FromStr, 25 | D: Deserializer<'de>, 26 | { 27 | let string = String::deserialize(deserializer)?; 28 | string 29 | .parse() 30 | .map_err(|_| D::Error::custom("Input is not valid")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.76.0" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Merge similar crates together to avoid multiple use statements. 2 | imports_granularity = "Module" 3 | 4 | # Consistency in formatting makes tool based searching/editing better. 5 | empty_item_single_line = false 6 | 7 | # Easier editing when arbitrary mixed use statements do not collapse. 8 | imports_layout = "Vertical" 9 | 10 | # Default rustfmt formatting of match arms with branches is awful. 11 | match_arm_leading_pipes = "Preserve" 12 | 13 | # Align Fields 14 | enum_discrim_align_threshold = 80 15 | struct_field_align_threshold = 80 16 | 17 | # Allow up to two blank lines for grouping. 18 | blank_lines_upper_bound = 2 19 | 20 | # Wrap comments 21 | comment_width = 120 22 | wrap_comments = true 23 | --------------------------------------------------------------------------------