├── .github └── workflows │ ├── add-to-devtools.yml │ ├── release-plz.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── cspell.json ├── examples ├── account_key_pooling.rs ├── contract_source_metadata.rs ├── create_account_and_send_near.rs ├── deploy_and_call_method.rs ├── ft.rs ├── global_deploy.rs ├── nep_413_signing_message.rs ├── nft.rs ├── sign_options.rs ├── specify_backup_rpc.rs └── specifying_block.rs ├── resources ├── counter.wasm ├── fungible_token.wasm └── nft.wasm ├── rust-toolchain.toml ├── src ├── account │ ├── create.rs │ └── mod.rs ├── chain.rs ├── common │ ├── mod.rs │ ├── query.rs │ ├── send.rs │ └── utils.rs ├── config.rs ├── contract.rs ├── errors.rs ├── fastnear.rs ├── lib.rs ├── signer │ ├── keystore.rs │ ├── ledger.rs │ ├── mod.rs │ └── secret_key.rs ├── stake.rs ├── storage.rs ├── tokens.rs ├── transactions.rs └── types │ ├── contract.rs │ ├── mod.rs │ ├── reference.rs │ ├── signed_delegate_action.rs │ ├── stake.rs │ ├── storage.rs │ ├── tokens.rs │ └── transactions.rs └── tests ├── account.rs ├── contract.rs ├── global_contracts.rs └── multiple_tx_at_same_time_from_same-_user.rs /.github/workflows/add-to-devtools.yml: -------------------------------------------------------------------------------- 1 | name: 'Add to DevTools Project' 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | pull_request_target: 9 | types: 10 | - opened 11 | - reopened 12 | 13 | jobs: 14 | add-to-project: 15 | name: Add issue/PR to project 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/add-to-project@v1.0.0 19 | with: 20 | # add to DevTools Project #156 21 | project-url: https://github.com/orgs/near/projects/156 22 | github-token: ${{ secrets.PROJECT_GH_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | token: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }} 22 | - name: Install Rust toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | - name: Install libudev (Linux only) 25 | run: sudo apt update && sudo apt-get -y install libudev-dev libsystemd-dev 26 | - name: Run release-plz 27 | uses: MarcoIeni/release-plz-action@v0.5 28 | env: 29 | # https://marcoieni.github.io/release-plz/github-action.html#triggering-further-workflow-runs 30 | GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }} 31 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test & Release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | RUSTFLAGS: '-D warnings' 11 | CARGO_INCREMENTAL: 0 12 | RUST_BACKTRACE: short 13 | 14 | jobs: 15 | clippy: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Install libudev (Linux only) 21 | if: runner.os == 'Linux' 22 | run: sudo apt-get update && sudo apt-get -y install libudev-dev libsystemd-dev 23 | - uses: Swatinem/rust-cache@v2 24 | - name: Run clippy 25 | run: cargo clippy --all-targets -- -D clippy::all -D clippy::nursery 26 | 27 | cargo-fmt: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Run cargo fmt 33 | run: cargo fmt --check 34 | 35 | cargo-doc: 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | - name: Install libudev (Linux only) 41 | if: runner.os == 'Linux' 42 | run: sudo apt-get update && sudo apt-get -y install libudev-dev libsystemd-dev 43 | - name: run cargo doc 44 | run: RUSTDOCFLAGS="-D warnings" cargo doc --all-features --document-private-items 45 | 46 | check-windows: 47 | needs: cargo-fmt 48 | runs-on: windows-latest 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: Swatinem/rust-cache@v2 53 | - name: Run cargo check 54 | run: cargo check --release 55 | 56 | no_features_check: 57 | needs: cargo-fmt 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v4 61 | - uses: Swatinem/rust-cache@v2 62 | - name: Run cargo check 63 | run: cargo check --no-default-features 64 | 65 | get_msrv: 66 | runs-on: ubuntu-latest 67 | outputs: 68 | version: ${{ steps.rust_msrv.outputs.version }} 69 | steps: 70 | - uses: actions/checkout@v4 71 | - name: Get MSRV 72 | id: rust_msrv 73 | run: | 74 | RUST_MSRV="$(cat Cargo.toml | sed -n 's/rust-version *= *"\(.*\)"/\1/p')" 75 | echo "Found MSRV: $RUST_MSRV" 76 | echo "version=$RUST_MSRV" >> "$GITHUB_OUTPUT" 77 | 78 | test: 79 | needs: [cargo-fmt, get_msrv] 80 | strategy: 81 | fail-fast: false 82 | matrix: 83 | platform: [ubuntu-latest, macos-latest] 84 | toolchain: 85 | - stable 86 | - ${{ needs.get_msrv.outputs.version }} 87 | runs-on: ${{ matrix.platform }} 88 | name: CI with ${{ matrix.toolchain }} 89 | steps: 90 | - uses: actions/checkout@v4 91 | - name: "${{ matrix.toolchain }}" 92 | uses: actions-rs/toolchain@v1 93 | with: 94 | profile: minimal 95 | toolchain: ${{ matrix.toolchain }} 96 | default: true 97 | - uses: Swatinem/rust-cache@v2 98 | - name: Install libudev (Linux only) 99 | if: runner.os == 'Linux' 100 | run: sudo apt-get update && sudo apt-get -y install libudev-dev libsystemd-dev 101 | - name: Check with stable features 102 | run: cargo check --examples --all-features 103 | - name: Run tests 104 | run: cargo test 105 | spellcheck: 106 | runs-on: ubuntu-latest 107 | steps: 108 | - uses: actions/checkout@v4 109 | - uses: streetsidesoftware/cspell-action@v6 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | seed_phrase 3 | contract_rs.wasm 4 | shell.nix 5 | .envrc 6 | .direnv 7 | Cargo.lock 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.6.0](https://github.com/near/near-api-rs/compare/v0.5.0...v0.6.0) - 2025-05-16 11 | 12 | ### Added 13 | 14 | - [**breaking**] added support for the s Global Contracts (NEP-591) ([#56](https://github.com/near/near-api-rs/pull/56)) 15 | - [**breaking**] add field output_wasm_path to ContractSourceMetadata ([#55](https://github.com/near/near-api-rs/pull/55)) 16 | - add issues & prs to devtools project ([#52](https://github.com/near/near-api-rs/pull/52)) 17 | 18 | ### Fixed 19 | 20 | - allow forks to leverage transfer-to-project workflow ([#54](https://github.com/near/near-api-rs/pull/54)) 21 | 22 | ### Other 23 | 24 | - [**breaking**] updates near-* dependencies to 0.30 release ([#59](https://github.com/near/near-api-rs/pull/59)) 25 | - *(near-contract-standards)* deserialize ContractSourceMetadata::standards field so, as if it were optional 26 | 27 | ## [0.5.0](https://github.com/near/near-api-rs/compare/v0.4.0...v0.5.0) - 2025-03-16 28 | 29 | ### Added 30 | 31 | - added `map` method to query builders ([#45](https://github.com/near/near-api-rs/pull/45)) 32 | - *(types::contract)* add `BuildInfo` field to `ContractSourceMetadata` ([#46](https://github.com/near/near-api-rs/pull/46)) 33 | - [**breaking**] NEP-413 support ([#37](https://github.com/near/near-api-rs/pull/37)) 34 | 35 | ### Other 36 | 37 | - [**breaking**] updates near-* dependencies to 0.29 release ([#51](https://github.com/near/near-api-rs/pull/51)) 38 | - added rust backward compatibility job, updated project readme ([#48](https://github.com/near/near-api-rs/pull/48)) 39 | - [**breaking**] documented types ([#44](https://github.com/near/near-api-rs/pull/44)) 40 | - added cargo words to supported dictionary ([#43](https://github.com/near/near-api-rs/pull/43)) 41 | - [**breaking**] added spellcheck ([#42](https://github.com/near/near-api-rs/pull/42)) 42 | - [**breaking**] documented all the builders. API changes ([#39](https://github.com/near/near-api-rs/pull/39)) 43 | - documented network config ([#35](https://github.com/near/near-api-rs/pull/35)) 44 | 45 | ## [0.4.0](https://github.com/near/near-api-rs/compare/v0.3.0...v0.4.0) - 2024-12-19 46 | 47 | ### Added 48 | 49 | - added ability to specify backup rpc for connecting to the network (#28) 50 | - don't retry on critical errors (query, tx) (#27) 51 | 52 | ### Other 53 | 54 | - updates near-* dependencies to 0.28 release. Removed Cargo.lock (#33) 55 | - [**breaking**] added documentation for root level and signer module (#32) 56 | - added CODEOWNERS (#31) 57 | - removed prelude and filtered entries. (#29) 58 | - replaced SecretBuilder with utility functions (#26) 59 | - [**breaking**] replaced deploy method as a static method (#18) 60 | 61 | ## [0.3.0](https://github.com/near/near-api-rs/compare/v0.2.1...v0.3.0) - 2024-11-19 62 | 63 | ### Added 64 | - added querying block, block hash, and block number ([#9](https://github.com/near/near-api-rs/pull/9)) 65 | - added prelude module ([#9](https://github.com/near/near-api-rs/pull/9)) 66 | 67 | ### Other 68 | - [**breaking**] updated near-* dependencies to 0.27 release ([#13](https://github.com/near/near-api-rs/pull/13)) 69 | 70 | ## [0.2.1](https://github.com/near/near-api-rs/compare/v0.2.0...v0.2.1) - 2024-10-25 71 | 72 | ### Added 73 | 74 | - added retry to querying. Simplified retry logic. ([#7](https://github.com/near/near-api-rs/pull/7)) 75 | 76 | ### Other 77 | 78 | - Update README.md 79 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dj8yfo @akorchyn @PolyProgrammist 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "near-api" 3 | version = "0.6.0" 4 | rust-version = "1.85" 5 | authors = [ 6 | "akorchyn ", 7 | "frol ", 8 | "Near Inc ", 9 | ] 10 | license = "MIT OR Apache-2.0" 11 | edition = "2021" 12 | repository = "https://github.com/near/near-api-rs" 13 | description = "Rust library to interact with NEAR Protocol via RPC API" 14 | 15 | exclude = ["resources", "tests"] 16 | 17 | [package.metadata.docs.rs] 18 | all-features = true 19 | rustdoc-args = ["--document-private-items"] 20 | 21 | [dependencies] 22 | borsh = "1.5" 23 | async-trait = "0.1" 24 | 25 | reqwest = { version = "0.12", features = ["blocking", "json"] } 26 | futures = "0.3" 27 | # Ad-hoc fix for compilation errors (rustls is used instead of openssl to ease the deployment avoiding the system dependency on openssl) 28 | openssl = { version = "0.10", features = ["vendored"] } 29 | 30 | bip39 = { version = "2.0.0", features = ["rand"] } 31 | ed25519-dalek = { version = "2", default-features = false } 32 | serde = { version = "1.0", features = ["derive"] } 33 | serde_json = "1.0.57" 34 | slipped10 = { version = "0.4.6" } 35 | url = { version = "2", features = ["serde"] } 36 | tokio = { version = "1.0", default-features = false, features = ["time"] } 37 | tracing = "0.1" 38 | bs58 = "0.4" 39 | 40 | thiserror = "2" 41 | 42 | near-ledger = { version = "0.8.1", optional = true } 43 | 44 | near-crypto = "0.30" 45 | near-primitives = "0.30" 46 | near-jsonrpc-client = "0.17" 47 | near-jsonrpc-primitives = "0.30" 48 | near-contract-standards = "5.14" 49 | near-sdk = "5.14" 50 | 51 | near-account-id = "1.0.0" 52 | near-gas = { version = "0.3", features = ["serde", "borsh"] } 53 | near-token = { version = "0.3", features = ["serde", "borsh"] } 54 | 55 | near-abi = "0.4.2" 56 | zstd = "0.13" 57 | 58 | keyring = { version = "3.2", features = [ 59 | "apple-native", 60 | "windows-native", 61 | "sync-secret-service", 62 | "vendored", 63 | ], optional = true } 64 | 65 | near-workspaces = { version = "0.20.0", optional = true } 66 | 67 | 68 | [features] 69 | default = ["ledger", "keystore"] 70 | ledger = ["near-ledger"] 71 | keystore = ["dep:keyring"] 72 | workspaces = ["dep:near-workspaces"] 73 | 74 | [dev-dependencies] 75 | tokio = { version = "1.0", default-features = false, features = ["full"] } 76 | near-api = { path = ".", features = ["workspaces"] } 77 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # near-api 2 |

3 | Reference Documentation 4 | Crates.io version 5 | Download 6 | Join the community on Discord 7 | Join the community on Telegram 8 |

9 | 10 | The `near-api` is a simple Rust library that helps developers interact easily with the NEAR blockchain. The library was highly inspired by the API of the [`near-cli-rs`](https://github.com/near/near-cli-rs) library. The library extensively utilizes builder patterns, this way we guide the users through the user flow, preventing most of the errors and focusing on each step. 11 | 12 | Currently, the library provides: 13 | * Account management 14 | * Contract deployment and interaction 15 | * NEAR, FT, NFT transfers 16 | * Storage deposit management 17 | * Stake management 18 | * Ability to create custom transactions 19 | * Several ways to sign transactions (secret key, seed phrase, file, ledger, secure keychain). 20 | * Account key pool support to sign the transaction with different user keys to avoid nonce issues. 21 | 22 | The minimum required version is located in the [rust-version](./Cargo.toml#L4) field of the `Cargo.toml` file. 23 | 24 | ## Features 25 | 26 | * `ledger`: Enables integration with a Ledger hardware signer for secure key management. 27 | * `keystore`: Enables integration with a system keystore signer for managing keys securely on the local system. 28 | * `workspaces`: Provides integration with [`near-workspaces`](https://github.com/near/near-workspaces-rs) for testing purposes. This feature allows you to convert `near-workspaces` networks (such as sandbox, testnet, etc.) into a NetworkConfig and use `near-workspaces` `Account` object as a signer for testing and development. 29 | 30 | ## Current issues 31 | 32 | The library is in good condition, but lacks a few points to be even better: 33 | - [x] documentation 34 | - [ ] good quality examples 35 | - [ ] integration tests for all API calls 36 | - [x] CI 37 | - [x] anyhow -> thiserror 38 | - [x] ledger is blocking and it's not good in the async runtime 39 | - [ ] secure keychain is not that straightforward to use 40 | - [x] storage deposit manager for FT calls 41 | - [x] basic logging with tracing for querying/signing/sending transactions 42 | - [ ] self-sustainable. remove the `nearcore` as a dependency ([#5](https://github.com/near/near-api-rs/issues/5)) 43 | 44 | ## Examples 45 | The crate provides [examples](./examples/) that contain detailed information on using the library. 46 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "ignorePaths": [], 4 | "dictionaryDefinitions": [], 5 | "dictionaries": [], 6 | "words": [ 7 | "libudev", 8 | "libsystemd", 9 | "clippy", 10 | "RUSTDOCFLAGS", 11 | "RUSTFLAGS", 12 | "Swatinem", 13 | "reqwest", 14 | "serde_json", 15 | "prepopulate", 16 | "transactionable", 17 | "linkdrop", 18 | "tgas", 19 | "yoctonear", 20 | "devhub", 21 | "presign", 22 | "borsh", 23 | "pubkeys", 24 | "keypair", 25 | "dalek", 26 | "delegators", 27 | "unstaked", 28 | "fastnear", 29 | "poolv1", 30 | "thiserror", 31 | "APDU", 32 | "multiquery", 33 | "unstake", 34 | "unstaking", 35 | "milli", 36 | "millinear", 37 | "serde", 38 | "rustdoc", 39 | "rustls", 40 | "zstd", 41 | "codeowners", 42 | "akorchyn", 43 | "frol", 44 | "ipfs", 45 | "nep", 46 | "cid", 47 | "wnear", 48 | "backlinks", 49 | "sourcescan", 50 | "msrv", 51 | "nearcore", 52 | "dtolnay", 53 | "Ieni" 54 | ], 55 | "ignoreRegExpList": [ 56 | // Base58 encoded public key/secret key 57 | "\"ed25519:[-A-Za-z0-9+/]*={0,3}\"", 58 | "Email", 59 | ], 60 | "import": [] 61 | } 62 | -------------------------------------------------------------------------------- /examples/account_key_pooling.rs: -------------------------------------------------------------------------------- 1 | /// You can use account key pooling to use different keys for consecutive transactions 2 | /// to avoid nonce-related issues. 3 | /// 4 | /// This is an example of how to use account key pooling to send multiple transactions 5 | /// using different keys. 6 | use near_api::*; 7 | use near_token::NearToken; 8 | use signer::generate_secret_key; 9 | 10 | use std::sync::Arc; 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | let network = near_workspaces::sandbox().await.unwrap(); 15 | let account = network.dev_create_account().await.unwrap(); 16 | let second_account = network.dev_create_account().await.unwrap(); 17 | let network = NetworkConfig::from(network); 18 | 19 | let signer = Signer::new(Signer::from_workspace(&account)).unwrap(); 20 | 21 | println!( 22 | "Initial public key: {}", 23 | signer.get_public_key().await.unwrap() 24 | ); 25 | 26 | let secret_key = generate_secret_key().unwrap(); 27 | println!("New public key: {}", secret_key.public_key()); 28 | 29 | Account(account.id().clone()) 30 | .add_key( 31 | near_primitives::account::AccessKeyPermission::FullAccess, 32 | secret_key.public_key(), 33 | ) 34 | .with_signer(Arc::clone(&signer)) 35 | .send_to(&network) 36 | .await 37 | .unwrap() 38 | .assert_success(); 39 | 40 | signer 41 | .add_signer_to_pool(Signer::from_secret_key(secret_key)) 42 | .await 43 | .unwrap(); 44 | 45 | let txs = (0..2).map(|_| { 46 | Tokens::account(account.id().clone()) 47 | .send_to(second_account.id().clone()) 48 | .near(NearToken::from_near(1)) 49 | .with_signer(Arc::clone(&signer)) 50 | .send_to(&network) 51 | }); 52 | let results = futures::future::join_all(txs) 53 | .await 54 | .into_iter() 55 | .collect::, _>>() 56 | .unwrap(); 57 | 58 | assert_eq!(results.len(), 2); 59 | results.iter().for_each(|e| e.assert_success()); 60 | println!("All transactions are successful"); 61 | println!( 62 | "Transaction one public key: {}", 63 | results[0].transaction.public_key 64 | ); 65 | println!( 66 | "Transaction two public key: {}", 67 | results[1].transaction.public_key 68 | ); 69 | assert_ne!( 70 | results[0].transaction.public_key, 71 | results[1].transaction.public_key 72 | ); 73 | 74 | println!("All transactions are successful"); 75 | } 76 | -------------------------------------------------------------------------------- /examples/contract_source_metadata.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | use std::str::FromStr; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | for (account_name, expected_json_metadata) in [ 7 | ("desolate-toad.testnet", FIRST_METADATA), 8 | ("fat-fabulous-toad.testnet", SECOND_METADATA), 9 | ] { 10 | let source_metadata = Contract(AccountId::from_str(account_name).expect("no err")) 11 | .contract_source_metadata() 12 | .fetch_from_testnet() 13 | .await 14 | .expect("no network or rpc err"); 15 | 16 | assert_eq!( 17 | expected_json_metadata, 18 | serde_json::to_string_pretty(&source_metadata.data).expect("no ser err") 19 | ); 20 | } 21 | } 22 | 23 | const FIRST_METADATA: &str = r#"{ 24 | "version": "0.1.0", 25 | "link": "https://github.com/dj8yfo/quiet_glen", 26 | "standards": [ 27 | { 28 | "standard": "nep330", 29 | "version": "1.2.0" 30 | } 31 | ], 32 | "build_info": null 33 | }"#; 34 | 35 | const SECOND_METADATA: &str = r#"{ 36 | "version": "0.1.0", 37 | "link": "https://github.com/dj8yfo/quiet_glen/tree/8d8a8a0fe86a1d8eb3bce45f04ab1a65fecf5a1b", 38 | "standards": [ 39 | { 40 | "standard": "nep330", 41 | "version": "1.2.0" 42 | } 43 | ], 44 | "build_info": { 45 | "build_environment": "sourcescan/cargo-near:0.13.3-rust-1.84.0@sha256:722198ddb92d1b82cbfcd3a4a9f7fba6fd8715f4d0b5fb236d8725c4883f97de", 46 | "build_command": [ 47 | "cargo", 48 | "near", 49 | "build", 50 | "non-reproducible-wasm", 51 | "--locked" 52 | ], 53 | "contract_path": "", 54 | "source_code_snapshot": "git+https://github.com/dj8yfo/quiet_glen?rev=8d8a8a0fe86a1d8eb3bce45f04ab1a65fecf5a1b", 55 | "output_wasm_path": null 56 | } 57 | }"#; 58 | -------------------------------------------------------------------------------- /examples/create_account_and_send_near.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | 3 | use near_token::NearToken; 4 | use signer::generate_secret_key; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let network = near_workspaces::sandbox().await.unwrap(); 9 | let account = network.dev_create_account().await.unwrap(); 10 | let network = NetworkConfig::from(network); 11 | 12 | let balance = Tokens::account(account.id().clone()) 13 | .near_balance() 14 | .fetch_from(&network) 15 | .await 16 | .unwrap(); 17 | 18 | println!("Balance: {}", balance.total); 19 | 20 | let new_account: AccountId = format!("{}.{}", "bob", account.id()).parse().unwrap(); 21 | let signer = Signer::new(Signer::from_workspace(&account)).unwrap(); 22 | 23 | Account::create_account(new_account.clone()) 24 | .fund_myself(account.id().clone(), NearToken::from_near(1)) 25 | .public_key(generate_secret_key().unwrap().public_key()) 26 | .unwrap() 27 | .with_signer(signer.clone()) 28 | .send_to(&network) 29 | .await 30 | .unwrap(); 31 | 32 | Tokens::account(account.id().clone()) 33 | .send_to(new_account.clone()) 34 | .near(NearToken::from_near(1)) 35 | .with_signer(signer) 36 | .send_to(&network) 37 | .await 38 | .unwrap(); 39 | 40 | let new_account_balance = Tokens::account(account.id().clone()) 41 | .near_balance() 42 | .fetch_from(&network) 43 | .await 44 | .unwrap(); 45 | let bob_balance = Tokens::account(new_account) 46 | .near_balance() 47 | .fetch_from(&network) 48 | .await 49 | .unwrap(); 50 | 51 | println!("Balance: {}", new_account_balance.total); 52 | // Expect to see 2 NEAR in Bob's account. 1 NEAR from create_account and 1 NEAR from send_near 53 | println!("Bob balance: {}", bob_balance.total); 54 | } 55 | -------------------------------------------------------------------------------- /examples/deploy_and_call_method.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | let network = near_workspaces::sandbox().await.unwrap(); 6 | let account = network.dev_create_account().await.unwrap(); 7 | let network = NetworkConfig::from(network); 8 | 9 | let signer = Signer::new(Signer::from_workspace(&account)).unwrap(); 10 | 11 | // Let's deploy the contract. The contract is simple counter with `get_num`, `increase`, `decrease` arguments 12 | Contract::deploy(account.id().clone()) 13 | .use_code(include_bytes!("../resources/counter.wasm").to_vec()) 14 | // You can add init call as well using `with_init_call` 15 | .without_init_call() 16 | .with_signer(signer.clone()) 17 | .send_to(&network) 18 | .await 19 | .unwrap(); 20 | 21 | let contract = Contract(account.id().clone()); 22 | 23 | // Let's fetch current value on a contract 24 | let current_value: Data = contract 25 | // Please note that you can add any argument as long as it is deserializable by serde :) 26 | // feel free to use serde_json::json macro as well 27 | .call_function("get_num", ()) 28 | .unwrap() 29 | .read_only() 30 | .fetch_from(&network) 31 | .await 32 | .unwrap(); 33 | 34 | println!("Current value: {}", current_value.data); 35 | 36 | // Here is a transaction that require signing compared to view call that was used before. 37 | contract 38 | .call_function("increment", ()) 39 | .unwrap() 40 | .transaction() 41 | .with_signer(account.id().clone(), signer.clone()) 42 | .send_to(&network) 43 | .await 44 | .unwrap() 45 | .assert_success(); 46 | 47 | let current_value: Data = contract 48 | .call_function("get_num", ()) 49 | .unwrap() 50 | .read_only() 51 | .fetch_from(&network) 52 | .await 53 | .unwrap(); 54 | 55 | println!("Current value: {}", current_value.data); 56 | } 57 | -------------------------------------------------------------------------------- /examples/ft.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | 3 | use serde_json::json; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let network = near_workspaces::sandbox().await.unwrap(); 8 | let token = network.dev_create_account().await.unwrap(); 9 | let account = network.dev_create_account().await.unwrap(); 10 | let network = NetworkConfig::from(network); 11 | let token_signer = Signer::new(Signer::from_workspace(&token)).unwrap(); 12 | 13 | // Deploying token contract 14 | Contract::deploy(token.id().clone()) 15 | .use_code(include_bytes!("../resources/fungible_token.wasm").to_vec()) 16 | .with_init_call( 17 | "new_default_meta", 18 | json!({ 19 | "owner_id": token.id().to_string(), 20 | "total_supply": "1000000000000000000000000000" 21 | }), 22 | ) 23 | .unwrap() 24 | .with_signer(token_signer.clone()) 25 | .send_to(&network) 26 | .await 27 | .unwrap(); 28 | 29 | // Verifying that user has 1000 tokens 30 | let tokens = Tokens::account(token.id().clone()) 31 | .ft_balance(token.id().clone()) 32 | .unwrap() 33 | .fetch_from(&network) 34 | .await 35 | .unwrap(); 36 | 37 | println!("Owner has {}", tokens); 38 | 39 | // Transfer 100 tokens to the account 40 | // We handle internally the storage deposit for the receiver account 41 | Tokens::account(token.id().clone()) 42 | .send_to(account.id().clone()) 43 | .ft( 44 | token.id().clone(), 45 | // Send 1.5 tokens 46 | FTBalance::with_decimals(24).with_whole_amount(100), 47 | ) 48 | .unwrap() 49 | .with_signer(token_signer.clone()) 50 | .send_to(&network) 51 | .await 52 | .unwrap() 53 | .assert_success(); 54 | 55 | let tokens = Tokens::account(account.id().clone()) 56 | .ft_balance(token.id().clone()) 57 | .unwrap() 58 | .fetch_from(&network) 59 | .await 60 | .unwrap(); 61 | 62 | println!("Account has {}", tokens); 63 | 64 | let tokens = Tokens::account(token.id().clone()) 65 | .ft_balance(token.id().clone()) 66 | .unwrap() 67 | .fetch_from(&network) 68 | .await 69 | .unwrap(); 70 | 71 | println!("Owner has {}", tokens); 72 | 73 | // We validate decimals at the network level so this should fail with a validation error 74 | let token = Tokens::account(token.id().clone()) 75 | .send_to(account.id().clone()) 76 | .ft( 77 | token.id().clone(), 78 | FTBalance::with_decimals(8).with_whole_amount(100), 79 | ) 80 | .unwrap() 81 | .with_signer(token_signer) 82 | .send_to(&network) 83 | .await; 84 | 85 | assert!(token.is_err()); 86 | println!( 87 | "Expected decimal validation error: {}", 88 | token.err().unwrap() 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /examples/global_deploy.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | use near_primitives::hash::hash; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let network = near_workspaces::sandbox().await.unwrap(); 7 | let global = network.dev_create_account().await.unwrap(); 8 | let instance_of_global = network.dev_create_account().await.unwrap(); 9 | 10 | let network = NetworkConfig::from(network); 11 | 12 | let global_signer = Signer::new(Signer::from_workspace(&global)).unwrap(); 13 | let instance_of_global_signer = 14 | Signer::new(Signer::from_workspace(&instance_of_global)).unwrap(); 15 | 16 | let code: Vec = include_bytes!("../resources/counter.wasm").to_vec(); 17 | let contract_hash = hash(&code); 18 | 19 | Contract::deploy_global_contract_code(code.clone()) 20 | .as_hash() 21 | .with_signer(global.id().clone(), global_signer.clone()) 22 | .send_to(&network) 23 | .await 24 | .unwrap() 25 | .assert_success(); 26 | 27 | Contract::deploy_global_contract_code(code) 28 | .as_account_id(global.id().clone()) 29 | .with_signer(global_signer.clone()) 30 | .send_to(&network) 31 | .await 32 | .unwrap() 33 | .assert_success(); 34 | 35 | Contract::deploy(instance_of_global.id().clone()) 36 | .use_global_account_id(global.id().clone()) 37 | .without_init_call() 38 | .with_signer(instance_of_global_signer.clone()) 39 | .send_to(&network) 40 | .await 41 | .unwrap() 42 | .assert_success(); 43 | 44 | Contract::deploy(instance_of_global.id().clone()) 45 | .use_global_hash(contract_hash.into()) 46 | .without_init_call() 47 | .with_signer(instance_of_global_signer.clone()) 48 | .send_to(&network) 49 | .await 50 | .unwrap() 51 | .assert_success(); 52 | 53 | println!( 54 | "Successfully deployed contract using both global hash and global account ID methods!" 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /examples/nep_413_signing_message.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | 3 | use openssl::rand::rand_bytes; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let signer = Signer::from_seed_phrase( 8 | "fatal edge jacket cash hard pass gallery fabric whisper size rain biology", 9 | None, 10 | ) 11 | .unwrap(); 12 | 13 | let mut nonce = [0u8; 32]; 14 | rand_bytes(&mut nonce).unwrap(); 15 | 16 | let payload = near_api::signer::NEP413Payload { 17 | message: "Hello NEAR!".to_string(), 18 | nonce, 19 | recipient: "example.near".to_string(), 20 | callback_url: None, 21 | }; 22 | 23 | let signature = signer 24 | .sign_message_nep413( 25 | "round-toad.testnet".parse().unwrap(), 26 | signer.get_public_key().unwrap(), 27 | payload, 28 | ) 29 | .await 30 | .unwrap(); 31 | 32 | println!("Signature: {}", signature); 33 | } 34 | -------------------------------------------------------------------------------- /examples/nft.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | 3 | use near_contract_standards::non_fungible_token::metadata::TokenMetadata; 4 | use serde_json::json; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let network = near_workspaces::sandbox().await.unwrap(); 9 | let nft = network.dev_create_account().await.unwrap(); 10 | let account = network.dev_create_account().await.unwrap(); 11 | let account2 = network.dev_create_account().await.unwrap(); 12 | let network = NetworkConfig::from(network); 13 | 14 | let nft_signer = Signer::new(Signer::from_workspace(&nft)).unwrap(); 15 | 16 | // Deploying token contract 17 | Contract::deploy(nft.id().clone()) 18 | .use_code(include_bytes!("../resources/nft.wasm").to_vec()) 19 | .with_init_call( 20 | "new_default_meta", 21 | json!({ 22 | "owner_id": nft.id().to_string(), 23 | }), 24 | ) 25 | .unwrap() 26 | .with_signer(nft_signer.clone()) 27 | .send_to(&network) 28 | .await 29 | .unwrap(); 30 | 31 | let contract = Contract(nft.id().clone()); 32 | 33 | // Mint NFT via contract call 34 | contract 35 | .call_function( 36 | "nft_mint", 37 | json!({ 38 | "token_id": "1", 39 | "receiver_id": account.id().to_string(), 40 | "token_metadata": TokenMetadata { 41 | title: Some("My NFT".to_string()), 42 | description: Some("My first NFT".to_string()), 43 | ..Default::default() 44 | } 45 | }), 46 | ) 47 | .unwrap() 48 | .transaction() 49 | .deposit(NearToken::from_millinear(100)) 50 | .with_signer(nft.id().clone(), nft_signer.clone()) 51 | .send_to(&network) 52 | .await 53 | .unwrap(); 54 | 55 | // Verifying that account has our nft token 56 | let tokens = Tokens::account(account.id().clone()) 57 | .nft_assets(nft.id().clone()) 58 | .unwrap() 59 | .fetch_from(&network) 60 | .await 61 | .unwrap(); 62 | 63 | assert_eq!(tokens.data.len(), 1); 64 | println!("Account has {}", tokens.data.first().unwrap().token_id); 65 | 66 | Tokens::account(account.id().clone()) 67 | .send_to(account2.id().clone()) 68 | .nft(nft.id().clone(), "1".to_string()) 69 | .unwrap() 70 | .with_signer(nft_signer.clone()) 71 | .send_to(&network) 72 | .await 73 | .unwrap(); 74 | 75 | // Verifying that account doesn't have nft anymore 76 | let tokens = Tokens::account(account.id().clone()) 77 | .nft_assets(nft.id().clone()) 78 | .unwrap() 79 | .fetch_from(&network) 80 | .await 81 | .unwrap(); 82 | 83 | assert!(tokens.data.is_empty()); 84 | 85 | let tokens = Tokens::account(account2.id().clone()) 86 | .nft_assets(nft.id().clone()) 87 | .unwrap() 88 | .fetch_from(&network) 89 | .await 90 | .unwrap(); 91 | 92 | assert_eq!(tokens.data.len(), 1); 93 | println!("account 2 has {}", tokens.data.first().unwrap().token_id); 94 | } 95 | -------------------------------------------------------------------------------- /examples/sign_options.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | use near_crypto::SecretKey; 3 | use near_primitives::account::AccessKeyPermission; 4 | use signer::generate_seed_phrase; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let network = near_workspaces::sandbox().await.unwrap(); 9 | let account = network.dev_create_account().await.unwrap(); 10 | let network = NetworkConfig::from(network); 11 | 12 | // Current secret key from workspace 13 | let current_secret_key: SecretKey = account.secret_key().to_string().parse().unwrap(); 14 | let (new_seed_phrase, public_key) = generate_seed_phrase().unwrap(); 15 | 16 | // Let's add new key and get the seed phrase 17 | Account(account.id().clone()) 18 | .add_key(AccessKeyPermission::FullAccess, public_key) 19 | .with_signer(Signer::new(Signer::from_secret_key(current_secret_key.clone())).unwrap()) 20 | .send_to(&network) 21 | .await 22 | .unwrap(); 23 | 24 | // Let's add ledger to the account with the new seed phrase 25 | let ledger_pubkey = Signer::from_ledger().get_public_key().unwrap(); 26 | Account(account.id().clone()) 27 | .add_key(AccessKeyPermission::FullAccess, ledger_pubkey) 28 | .with_signer( 29 | Signer::new(Signer::from_seed_phrase(&new_seed_phrase, Some("smile")).unwrap()) 30 | .unwrap(), 31 | ) 32 | .send_to(&network) 33 | .await 34 | .unwrap(); 35 | 36 | println!("Signing with ledger"); 37 | // Let's sign some tx with the ledger key 38 | Account(account.id().clone()) 39 | .delete_key(current_secret_key.public_key()) 40 | .with_signer(Signer::new(Signer::from_ledger()).unwrap()) 41 | .send_to(&network) 42 | .await 43 | .unwrap(); 44 | 45 | let keys = Account(account.id().clone()) 46 | .list_keys() 47 | .fetch_from(&network) 48 | .await 49 | .unwrap(); 50 | 51 | // Should contain 2 keys: new key from seed phrase, and ledger key 52 | println!("{:#?}", keys); 53 | } 54 | -------------------------------------------------------------------------------- /examples/specify_backup_rpc.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | let mut network = NetworkConfig::mainnet(); 6 | network.rpc_endpoints.push( 7 | RPCEndpoint::new("https://rpc.mainnet.pagoda.co/".parse().unwrap()) 8 | .with_api_key("potential api key".parse().unwrap()) 9 | .with_retries(5), 10 | ); 11 | // Query latest block 12 | let _block = Chain::block() 13 | .at(Reference::Optimistic) 14 | .fetch_from_mainnet() 15 | .await 16 | .unwrap(); 17 | } 18 | -------------------------------------------------------------------------------- /examples/specifying_block.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | // Query latest block 6 | let _block = Chain::block() 7 | .at(Reference::Optimistic) 8 | .fetch_from_mainnet() 9 | .await 10 | .unwrap(); 11 | 12 | let block_number = Chain::block_number().fetch_from_mainnet().await.unwrap(); 13 | let block_hash = Chain::block_hash().fetch_from_mainnet().await.unwrap(); 14 | 15 | let _block = Chain::block() 16 | .at(Reference::AtBlock(block_number)) 17 | .fetch_from_mainnet() 18 | .await 19 | .unwrap(); 20 | 21 | let _block = Chain::block() 22 | .at(Reference::AtBlockHash(block_hash)) 23 | .fetch_from_mainnet() 24 | .await 25 | .unwrap(); 26 | } 27 | -------------------------------------------------------------------------------- /resources/counter.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near/near-api-rs/f5de08f1f0ef9ba7e721d450a115856423a56082/resources/counter.wasm -------------------------------------------------------------------------------- /resources/fungible_token.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near/near-api-rs/f5de08f1f0ef9ba7e721d450a115856423a56082/resources/fungible_token.wasm -------------------------------------------------------------------------------- /resources/nft.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near/near-api-rs/f5de08f1f0ef9ba7e721d450a115856423a56082/resources/nft.wasm -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | # This specifies the version of Rust we use to build. 3 | # Individual crates in the workspace may support a lower version, as indicated by `rust-version` field in each crate's `Cargo.toml`. 4 | # The version specified below, should be at least as high as the maximum `rust-version` within the workspace. 5 | channel = "stable" 6 | components = ["rustfmt", "clippy", "rust-analyzer"] 7 | targets = ["wasm32-unknown-unknown"] 8 | -------------------------------------------------------------------------------- /src/account/create.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use near_crypto::PublicKey; 4 | use near_gas::NearGas; 5 | use near_primitives::types::AccountId; 6 | use near_token::NearToken; 7 | use reqwest::Response; 8 | use serde_json::json; 9 | use url::Url; 10 | 11 | use crate::{ 12 | common::send::Transactionable, 13 | errors::{AccountCreationError, FaucetError, ValidationError}, 14 | transactions::{ConstructTransaction, TransactionWithSign}, 15 | types::transactions::PrepopulateTransaction, 16 | Contract, NetworkConfig, 17 | }; 18 | 19 | #[derive(Clone, Debug)] 20 | pub struct CreateAccountBuilder { 21 | pub account_id: AccountId, 22 | } 23 | 24 | impl CreateAccountBuilder { 25 | /// Create an NEAR account and fund it by your own 26 | /// 27 | /// You can only create an sub-account of your own account or sub-account of the linkdrop account ([near](https://nearblocks.io/address/near) on mainnet , [testnet](https://testnet.nearblocks.io/address/testnet) on testnet) 28 | pub fn fund_myself( 29 | self, 30 | signer_account_id: AccountId, 31 | initial_balance: NearToken, 32 | ) -> PublicKeyProvider, AccountCreationError> 33 | { 34 | PublicKeyProvider::new(Box::new(move |public_key| { 35 | let (actions, receiver_id) = if self.account_id.is_sub_account_of(&signer_account_id) { 36 | ( 37 | vec![ 38 | near_primitives::transaction::Action::CreateAccount( 39 | near_primitives::transaction::CreateAccountAction {}, 40 | ), 41 | near_primitives::transaction::Action::Transfer( 42 | near_primitives::transaction::TransferAction { 43 | deposit: initial_balance.as_yoctonear(), 44 | }, 45 | ), 46 | near_primitives::transaction::Action::AddKey(Box::new( 47 | near_primitives::transaction::AddKeyAction { 48 | public_key, 49 | access_key: near_primitives::account::AccessKey { 50 | nonce: 0, 51 | permission: 52 | near_primitives::account::AccessKeyPermission::FullAccess, 53 | }, 54 | }, 55 | )), 56 | ], 57 | self.account_id.clone(), 58 | ) 59 | } else if let Some(linkdrop_account_id) = self.account_id.get_parent_account_id() { 60 | ( 61 | Contract(linkdrop_account_id.to_owned()) 62 | .call_function( 63 | "create_account", 64 | json!({ 65 | "new_account_id": self.account_id.to_string(), 66 | "new_public_key": public_key.to_string(), 67 | }), 68 | )? 69 | .transaction() 70 | .gas(NearGas::from_tgas(30)) 71 | .deposit(initial_balance) 72 | .with_signer_account(signer_account_id.clone()) 73 | .prepopulated() 74 | .actions, 75 | linkdrop_account_id.to_owned(), 76 | ) 77 | } else { 78 | return Err(AccountCreationError::TopLevelAccountIsNotAllowed); 79 | }; 80 | 81 | let prepopulated = ConstructTransaction::new(signer_account_id, receiver_id) 82 | .add_actions(actions) 83 | .prepopulated(); 84 | 85 | Ok(TransactionWithSign { 86 | tx: CreateAccountFundMyselfTx { prepopulated }, 87 | }) 88 | })) 89 | } 90 | 91 | /// Create an account sponsored by faucet service 92 | /// 93 | /// This is a way to create an account without having to fund it. It works only on testnet. 94 | /// You can only create an sub-account of the [testnet](https://testnet.nearblocks.io/address/testnet) account 95 | pub fn sponsor_by_faucet_service(self) -> PublicKeyProvider { 96 | PublicKeyProvider::new(Box::new(move |public_key| { 97 | Ok(CreateAccountByFaucet { 98 | new_account_id: self.account_id, 99 | public_key, 100 | }) 101 | })) 102 | } 103 | } 104 | 105 | #[derive(Clone, Debug)] 106 | pub struct CreateAccountByFaucet { 107 | pub new_account_id: AccountId, 108 | pub public_key: PublicKey, 109 | } 110 | 111 | impl CreateAccountByFaucet { 112 | /// Sends the account creation request to the default testnet faucet service. 113 | /// 114 | /// The account will be created as a sub-account of the [testnet](https://testnet.nearblocks.io/address/testnet) account 115 | pub async fn send_to_testnet_faucet(self) -> Result { 116 | let testnet = NetworkConfig::testnet(); 117 | self.send_to_config_faucet(&testnet).await 118 | } 119 | 120 | /// Sends the account creation request to the faucet service specified in the network config. 121 | /// This way you can specify your own faucet service. 122 | /// 123 | /// The function sends the request in the following format: 124 | /// ```json 125 | /// { 126 | /// "newAccountId": "new_account_id", 127 | /// "newAccountPublicKey": "new_account_public_key" 128 | /// } 129 | /// ``` 130 | pub async fn send_to_config_faucet( 131 | self, 132 | config: &NetworkConfig, 133 | ) -> Result { 134 | let faucet_service_url = match &config.faucet_url { 135 | Some(url) => url, 136 | None => return Err(FaucetError::FaucetIsNotDefined(config.network_name.clone())), 137 | }; 138 | 139 | self.send_to_faucet(faucet_service_url).await 140 | } 141 | 142 | /// Sends the account creation request to the faucet service specified by the URL. 143 | /// 144 | /// The function sends the request in the following format: 145 | /// ```json 146 | /// { 147 | /// "newAccountId": "new_account_id", 148 | /// "newAccountPublicKey": "new_account_public_key" 149 | /// } 150 | /// ``` 151 | pub async fn send_to_faucet(self, url: &Url) -> Result { 152 | let mut data = std::collections::HashMap::new(); 153 | data.insert("newAccountId", self.new_account_id.to_string()); 154 | data.insert("newAccountPublicKey", self.public_key.to_string()); 155 | 156 | let client = reqwest::Client::new(); 157 | 158 | Ok(client.post(url.clone()).json(&data).send().await?) 159 | } 160 | } 161 | 162 | #[derive(Clone, Debug)] 163 | pub struct CreateAccountFundMyselfTx { 164 | prepopulated: PrepopulateTransaction, 165 | } 166 | 167 | #[async_trait::async_trait] 168 | impl Transactionable for CreateAccountFundMyselfTx { 169 | fn prepopulated(&self) -> PrepopulateTransaction { 170 | self.prepopulated.clone() 171 | } 172 | 173 | async fn validate_with_network(&self, network: &NetworkConfig) -> Result<(), ValidationError> { 174 | if self 175 | .prepopulated 176 | .receiver_id 177 | .is_sub_account_of(&self.prepopulated.signer_id) 178 | { 179 | return Ok(()); 180 | } 181 | 182 | match &network.linkdrop_account_id { 183 | Some(linkdrop) => { 184 | if &self.prepopulated.receiver_id != linkdrop { 185 | Err(AccountCreationError::AccountShouldBeSubAccountOfSignerOrLinkdrop)?; 186 | } 187 | } 188 | None => Err(AccountCreationError::LinkdropIsNotDefined)?, 189 | } 190 | 191 | Ok(()) 192 | } 193 | } 194 | 195 | pub type PublicKeyCallback = dyn FnOnce(PublicKey) -> Result; 196 | 197 | pub struct PublicKeyProvider { 198 | next_step: Box>, 199 | } 200 | 201 | impl PublicKeyProvider { 202 | pub const fn new(next_step: Box>) -> Self { 203 | Self { next_step } 204 | } 205 | 206 | pub fn public_key(self, pk: PublicKey) -> Result { 207 | (self.next_step)(pk) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/account/mod.rs: -------------------------------------------------------------------------------- 1 | use near_crypto::PublicKey; 2 | use near_primitives::{ 3 | account::{AccessKey, AccessKeyPermission}, 4 | action::{AddKeyAction, DeleteKeyAction}, 5 | types::{AccountId, BlockReference}, 6 | }; 7 | 8 | use crate::common::query::{ 9 | AccessKeyHandler, AccessKeyListHandler, AccountViewHandler, QueryBuilder, RpcBuilder, 10 | SimpleQuery, 11 | }; 12 | use crate::transactions::ConstructTransaction; 13 | 14 | use self::create::CreateAccountBuilder; 15 | 16 | mod create; 17 | 18 | /// Account management related interactions with the NEAR Protocol 19 | /// 20 | /// The [`Account`] struct provides methods to interact with NEAR accounts, including querying account information, managing access keys, and creating new accounts. 21 | /// 22 | /// # Examples 23 | /// 24 | /// ```rust,no_run 25 | /// use near_api::*; 26 | /// 27 | /// # async fn example() -> Result<(), Box> { 28 | /// let account_info = Account("alice.testnet".parse()?).view().fetch_from_testnet().await?; 29 | /// println!("Account: {:?}", account_info); 30 | /// # Ok(()) 31 | /// # } 32 | /// ``` 33 | #[derive(Clone, Debug)] 34 | pub struct Account(pub AccountId); 35 | 36 | impl Account { 37 | /// Prepares a query to fetch the [Data](crate::Data)<[AccountView](near_primitives::views::AccountView)> with the account information for the given account ID. 38 | /// 39 | /// ## Example 40 | /// ```rust,no_run 41 | /// use near_api::*; 42 | /// 43 | /// # async fn example() -> Result<(), Box> { 44 | /// let account_info = Account("alice.testnet".parse()?).view().fetch_from_testnet().await?; 45 | /// println!("Account: {:?}", account_info); 46 | /// # Ok(()) 47 | /// # } 48 | /// ``` 49 | pub fn view(&self) -> QueryBuilder { 50 | let request = near_primitives::views::QueryRequest::ViewAccount { 51 | account_id: self.0.clone(), 52 | }; 53 | QueryBuilder::new( 54 | SimpleQuery { request }, 55 | BlockReference::latest(), 56 | Default::default(), 57 | ) 58 | } 59 | 60 | /// Prepares a query to fetch the [Data](crate::Data)<[AccessKeyView](near_primitives::views::AccessKeyView)> with the access key information for the given account public key. 61 | /// 62 | /// ## Example 63 | /// ```rust,no_run 64 | /// use near_api::*; 65 | /// use near_crypto::PublicKey; 66 | /// use std::str::FromStr; 67 | /// 68 | /// # async fn example() -> Result<(), Box> { 69 | /// let access_key = Account("alice.testnet".parse()?) 70 | /// .access_key(PublicKey::from_str("ed25519:H4sIAAAAAAAAA+2X0Q6CMBAAtVlJQgYAAAA=")?) 71 | /// .fetch_from_testnet() 72 | /// .await?; 73 | /// println!("Access key: {:?}", access_key); 74 | /// # Ok(()) 75 | /// # } 76 | /// ``` 77 | pub fn access_key(&self, signer_public_key: PublicKey) -> QueryBuilder { 78 | let request = near_primitives::views::QueryRequest::ViewAccessKey { 79 | account_id: self.0.clone(), 80 | public_key: signer_public_key, 81 | }; 82 | RpcBuilder::new( 83 | SimpleQuery { request }, 84 | BlockReference::latest(), 85 | Default::default(), 86 | ) 87 | } 88 | 89 | /// Prepares a query to fetch the [AccessKeyList](near_primitives::views::AccessKeyList) list of access keys for the given account ID. 90 | /// 91 | /// ## Example 92 | /// ```rust,no_run 93 | /// use near_api::*; 94 | /// 95 | /// # async fn example() -> Result<(), Box> { 96 | /// let access_keys = Account("alice.testnet".parse()?).list_keys().fetch_from_testnet().await?; 97 | /// println!("Access keys: {:?}", access_keys); 98 | /// # Ok(()) 99 | /// # } 100 | /// ``` 101 | pub fn list_keys(&self) -> QueryBuilder { 102 | let request = near_primitives::views::QueryRequest::ViewAccessKeyList { 103 | account_id: self.0.clone(), 104 | }; 105 | RpcBuilder::new( 106 | SimpleQuery { request }, 107 | BlockReference::latest(), 108 | Default::default(), 109 | ) 110 | } 111 | 112 | /// Adds a new access key to the given account ID. 113 | /// 114 | /// ## Example 115 | /// ```rust,no_run 116 | /// use near_api::*; 117 | /// use near_primitives::account::AccessKeyPermission; 118 | /// use near_crypto::PublicKey; 119 | /// use std::str::FromStr; 120 | /// 121 | /// # async fn example() -> Result<(), Box> { 122 | /// let pk = PublicKey::from_str("ed25519:H4sIAAAAAAAAA+2X0Q6CMBAAtVlJQgYAAAA=")?; 123 | /// let result: near_primitives::views::FinalExecutionOutcomeView = Account("alice.testnet".parse()?) 124 | /// .add_key(AccessKeyPermission::FullAccess, pk) 125 | /// .with_signer(Signer::new(Signer::from_ledger())?) 126 | /// .send_to_testnet() 127 | /// .await?; 128 | /// # Ok(()) 129 | /// # } 130 | /// ``` 131 | pub fn add_key( 132 | &self, 133 | permission: AccessKeyPermission, 134 | public_key: PublicKey, 135 | ) -> ConstructTransaction { 136 | ConstructTransaction::new(self.0.clone(), self.0.clone()).add_action( 137 | near_primitives::transaction::Action::AddKey(Box::new(AddKeyAction { 138 | access_key: AccessKey { 139 | nonce: 0, 140 | permission, 141 | }, 142 | public_key, 143 | })), 144 | ) 145 | } 146 | 147 | /// Deletes an access key from the given account ID. 148 | /// 149 | /// ## Example 150 | /// ```rust,no_run 151 | /// use near_api::*; 152 | /// use near_crypto::PublicKey; 153 | /// use std::str::FromStr; 154 | /// # async fn example() -> Result<(), Box> { 155 | /// let result: near_primitives::views::FinalExecutionOutcomeView = Account("alice.testnet".parse()?) 156 | /// .delete_key(PublicKey::from_str("ed25519:H4sIAAAAAAAAA+2X0Q6CMBAAtVlJQgYAAAA=")?) 157 | /// .with_signer(Signer::new(Signer::from_ledger())?) 158 | /// .send_to_testnet() 159 | /// .await?; 160 | /// # Ok(()) 161 | /// # } 162 | /// ``` 163 | pub fn delete_key(&self, public_key: PublicKey) -> ConstructTransaction { 164 | ConstructTransaction::new(self.0.clone(), self.0.clone()).add_action( 165 | near_primitives::transaction::Action::DeleteKey(Box::new(DeleteKeyAction { 166 | public_key, 167 | })), 168 | ) 169 | } 170 | 171 | /// Deletes multiple access keys from the given account ID. 172 | /// 173 | /// ## Example 174 | /// ```rust,no_run 175 | /// use near_api::*; 176 | /// use near_crypto::PublicKey; 177 | /// use std::str::FromStr; 178 | /// 179 | /// # async fn example() -> Result<(), Box> { 180 | /// let result: near_primitives::views::FinalExecutionOutcomeView = Account("alice.testnet".parse()?) 181 | /// .delete_keys(vec![PublicKey::from_str("ed25519:H4sIAAAAAAAAA+2X0Q6CMBAAtVlJQgYAAAA=")?]) 182 | /// .with_signer(Signer::new(Signer::from_ledger())?) 183 | /// .send_to_testnet() 184 | /// .await?; 185 | /// # Ok(()) 186 | /// # } 187 | /// ``` 188 | pub fn delete_keys(&self, public_keys: Vec) -> ConstructTransaction { 189 | let actions = public_keys 190 | .into_iter() 191 | .map(|public_key| { 192 | near_primitives::transaction::Action::DeleteKey(Box::new(DeleteKeyAction { 193 | public_key, 194 | })) 195 | }) 196 | .collect(); 197 | 198 | ConstructTransaction::new(self.0.clone(), self.0.clone()).add_actions(actions) 199 | } 200 | 201 | /// Deletes the account with the given beneficiary ID. The account balance will be transferred to the beneficiary. 202 | /// 203 | /// Please note that this action is irreversible. Also, you have to understand that another person could potentially 204 | /// re-create the account with the same name and pretend to be you on other websites that use your account ID as a unique identifier. 205 | /// (near.social, devhub proposal, etc) 206 | /// 207 | /// Do not use it unless you understand the consequences. 208 | /// 209 | /// ## Example 210 | /// ```rust,no_run 211 | /// use near_api::*; 212 | /// 213 | /// # async fn example() -> Result<(), Box> { 214 | /// let result: near_primitives::views::FinalExecutionOutcomeView = Account("alice.testnet".parse()?) 215 | /// .delete_account_with_beneficiary("bob.testnet".parse()?) 216 | /// .with_signer(Signer::new(Signer::from_ledger())?) 217 | /// .send_to_testnet() 218 | /// .await?; 219 | /// # Ok(()) 220 | /// # } 221 | /// ``` 222 | pub fn delete_account_with_beneficiary( 223 | &self, 224 | beneficiary_id: AccountId, 225 | ) -> ConstructTransaction { 226 | ConstructTransaction::new(self.0.clone(), self.0.clone()).add_action( 227 | near_primitives::transaction::Action::DeleteAccount( 228 | near_primitives::transaction::DeleteAccountAction { beneficiary_id }, 229 | ), 230 | ) 231 | } 232 | 233 | /// Creates a new account builder for the given account ID. 234 | /// 235 | /// ## Creating account sponsored by faucet service 236 | /// 237 | /// This is a way to create an account without having to fund it. It works only on testnet. 238 | /// The account should be created as a sub-account of the [testnet](https://testnet.nearblocks.io/address/testnet) account 239 | /// 240 | /// ```rust,no_run 241 | /// use near_api::*; 242 | /// 243 | /// # async fn example() -> Result<(), Box> { 244 | /// let secret = near_api::signer::generate_secret_key()?; 245 | /// let result: reqwest::Response = Account::create_account("alice.testnet".parse()?) 246 | /// .sponsor_by_faucet_service() 247 | /// .public_key(secret.public_key())? 248 | /// .send_to_testnet_faucet() 249 | /// .await?; 250 | /// // You have to save the secret key somewhere safe 251 | /// std::fs::write("secret.key", secret.to_string())?; 252 | /// # Ok(()) 253 | /// # } 254 | /// ``` 255 | /// 256 | /// ## Creating sub-account of the linkdrop root account funded by your own NEAR and signed by your account 257 | /// 258 | /// There is a few linkdrop root accounts that you can use to create sub-accounts. 259 | /// * For mainnet, you can use the [near](https://explorer.near.org/accounts/near) account. 260 | /// * For testnet, you can use the [testnet](https://testnet.nearblocks.io/address/testnet) account. 261 | /// 262 | /// ```rust,no_run 263 | /// use near_api::*; 264 | /// 265 | /// # async fn example() -> Result<(), Box> { 266 | /// let secret = near_api::signer::generate_secret_key()?; 267 | /// let bob_signer = Signer::new(Signer::from_seed_phrase("lucky barrel fall come bottom can rib join rough around subway cloth ", None)?)?; 268 | /// let result: near_primitives::views::FinalExecutionOutcomeView = Account::create_account("alice.testnet".parse()?) 269 | /// .fund_myself("bob.testnet".parse()?, NearToken::from_near(1)) 270 | /// .public_key(secret.public_key())? 271 | /// .with_signer(bob_signer) 272 | /// .send_to_testnet() 273 | /// .await?; 274 | /// # Ok(()) 275 | /// # } 276 | /// ``` 277 | /// 278 | /// ## Creating sub-account of your own account funded by your NEAR 279 | /// 280 | /// You can create only one level deep of sub-accounts. 281 | /// 282 | /// E.g you are `alice.testnet`, you can't create `sub.sub.alice.testnet`, but you can create `sub.alice.testnet`. 283 | /// Though, 'sub.alice.testnet' can create sub-accounts of its own. 284 | /// ```rust,no_run 285 | /// use near_api::*; 286 | /// 287 | /// # async fn example() -> Result<(), Box> { 288 | /// let secret = near_api::signer::generate_secret_key()?; 289 | /// let bob_signer = Signer::new(Signer::from_seed_phrase("lucky barrel fall come bottom can rib join rough around subway cloth ", None)?)?; 290 | /// let result: near_primitives::views::FinalExecutionOutcomeView = Account::create_account("sub.bob.testnet".parse()?) 291 | /// .fund_myself("bob.testnet".parse()?, NearToken::from_near(1)) 292 | /// .public_key(secret.public_key())? 293 | /// .with_signer(bob_signer) 294 | /// .send_to_testnet() 295 | /// .await?; 296 | /// # Ok(()) 297 | /// # } 298 | /// ``` 299 | pub const fn create_account(account_id: AccountId) -> CreateAccountBuilder { 300 | CreateAccountBuilder { account_id } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/chain.rs: -------------------------------------------------------------------------------- 1 | use near_primitives::types::{BlockHeight, BlockReference}; 2 | 3 | use crate::{ 4 | common::query::{BlockQueryBuilder, PostprocessHandler, RpcBlockHandler, SimpleBlockRpc}, 5 | types::CryptoHash, 6 | }; 7 | 8 | /// Chain-related interactions with the NEAR Protocol 9 | /// 10 | /// The [`Chain`] struct provides methods to interact with the NEAR blockchain 11 | /// 12 | /// # Examples 13 | /// 14 | /// ```rust,no_run 15 | /// use near_api::*; 16 | /// 17 | /// # async fn example() -> Result<(), Box> { 18 | /// let block_number = Chain::block_number().fetch_from_testnet().await?; 19 | /// println!("Current block number: {}", block_number); 20 | /// # Ok(()) 21 | /// # } 22 | /// ``` 23 | #[derive(Debug, Clone, Copy)] 24 | pub struct Chain; 25 | 26 | impl Chain { 27 | /// Set ups a query to fetch the [BlockHeight] of the current block 28 | /// 29 | /// ## Fetching the latest block number 30 | /// 31 | /// ```rust,no_run 32 | /// use near_api::*; 33 | /// 34 | /// # async fn example() -> Result<(), Box> { 35 | /// let block_number = Chain::block_number().fetch_from_testnet().await?; 36 | /// println!("Current block number: {}", block_number); 37 | /// # Ok(()) 38 | /// # } 39 | /// ``` 40 | /// 41 | /// ## Fetching the final block number 42 | /// 43 | /// ```rust,no_run 44 | /// use near_api::*; 45 | /// 46 | /// # async fn example() -> Result<(), Box> { 47 | /// let block_number = Chain::block_number().at(Reference::Final).fetch_from_testnet().await?; 48 | /// println!("Final block number: {}", block_number); 49 | /// # Ok(()) 50 | /// # } 51 | /// ``` 52 | pub fn block_number() -> BlockQueryBuilder> { 53 | BlockQueryBuilder::new(SimpleBlockRpc, BlockReference::latest(), RpcBlockHandler) 54 | .map(|data| data.header.height) 55 | } 56 | 57 | /// Set ups a query to fetch the [CryptoHash] of the block 58 | /// 59 | /// ## Fetching the latest block hash 60 | /// 61 | /// ```rust,no_run 62 | /// use near_api::*; 63 | /// 64 | /// # async fn example() -> Result<(), Box> { 65 | /// let block_hash = Chain::block_hash().fetch_from_testnet().await?; 66 | /// println!("Current block hash: {}", block_hash); 67 | /// # Ok(()) 68 | /// # } 69 | /// ``` 70 | /// 71 | /// ## Fetching the hash at a specific block number 72 | /// 73 | /// ```rust,no_run 74 | /// use near_api::*; 75 | /// 76 | /// # async fn example() -> Result<(), Box> { 77 | /// let block_hash = Chain::block_hash().at(Reference::AtBlock(1000000)).fetch_from_testnet().await?; 78 | /// println!("Block hash at block number 1000000: {}", block_hash); 79 | /// # Ok(()) 80 | /// # } 81 | /// ``` 82 | pub fn block_hash() -> BlockQueryBuilder> { 83 | BlockQueryBuilder::new(SimpleBlockRpc, BlockReference::latest(), RpcBlockHandler) 84 | .map(|data| data.header.hash.into()) 85 | } 86 | 87 | /// Set ups a query to fetch the [BlockView][near_primitives::views::BlockView] 88 | /// 89 | /// ## Fetching the latest block 90 | /// 91 | /// ```rust,no_run 92 | /// use near_api::*; 93 | /// 94 | /// # async fn example() -> Result<(), Box> { 95 | /// let block = Chain::block().fetch_from_testnet().await?; 96 | /// println!("Current block: {:?}", block); 97 | /// # Ok(()) 98 | /// # } 99 | /// ``` 100 | /// 101 | /// ## Fetching the block at a specific block number 102 | /// 103 | /// ```rust,no_run 104 | /// use near_api::*; 105 | /// 106 | /// # async fn example() -> Result<(), Box> { 107 | /// let block = Chain::block().at(Reference::AtBlock(1000000)).fetch_from_testnet().await?; 108 | /// println!("Block at block number 1000000: {:?}", block); 109 | /// # Ok(()) 110 | /// # } 111 | /// ``` 112 | /// 113 | /// ## Fetching the block at a specific block hash 114 | /// 115 | /// ```rust,no_run 116 | /// use near_api::*; 117 | /// 118 | /// # async fn example() -> Result<(), Box> { 119 | /// # let block_hash = near_api::types::CryptoHash::default(); 120 | /// let block = Chain::block().at(Reference::AtBlockHash(block_hash)).fetch_from_testnet().await?; 121 | /// println!("Block at block hash: {:?}", block); 122 | /// # Ok(()) 123 | /// # } 124 | /// ``` 125 | pub fn block() -> BlockQueryBuilder { 126 | BlockQueryBuilder::new(SimpleBlockRpc, BlockReference::latest(), RpcBlockHandler) 127 | } 128 | 129 | // TODO: chunk info 130 | } 131 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | use near_primitives::types::BlockHeight; 2 | 3 | const META_TRANSACTION_VALID_FOR_DEFAULT: BlockHeight = 1000; 4 | 5 | pub mod query; 6 | pub mod send; 7 | pub mod utils; 8 | -------------------------------------------------------------------------------- /src/common/utils.rs: -------------------------------------------------------------------------------- 1 | // https://github.com/near/near-token-rs/blob/3feafec624e7d1028ed00695f2acf87e1d823fa7/src/utils.rs#L1-L49 2 | 3 | use crate::errors::DecimalNumberParsingError; 4 | 5 | /// Converts [crate::Data]<[u128]>] to [crate::NearToken]. 6 | pub const fn near_data_to_near_token(data: crate::Data) -> crate::NearToken { 7 | crate::NearToken::from_yoctonear(data.data) 8 | } 9 | 10 | /// Parsing decimal numbers from `&str` type in `u128`. 11 | /// Function also takes a value of metric prefix in u128 type. 12 | /// `parse_str` use the `u128` type, and have the same max and min values. 13 | /// 14 | /// If the fractional part is longer than several zeros in the prefix, it will return the error `DecimalNumberParsingError::LongFractional`. 15 | /// 16 | /// If the string slice has invalid chars, it will return the error `DecimalNumberParsingError::InvalidNumber`. 17 | /// 18 | /// If the whole part of the number has a value more than the `u64` maximum value, it will return the error `DecimalNumberParsingError::LongWhole`. 19 | pub fn parse_decimal_number(s: &str, pref_const: u128) -> Result { 20 | let (int, fraction) = if let Some((whole, fractional)) = s.trim().split_once('.') { 21 | let int: u128 = whole 22 | .parse() 23 | .map_err(|_| DecimalNumberParsingError::InvalidNumber(s.to_owned()))?; 24 | let mut fraction: u128 = fractional 25 | .parse() 26 | .map_err(|_| DecimalNumberParsingError::InvalidNumber(s.to_owned()))?; 27 | let len = u32::try_from(fractional.len()) 28 | .map_err(|_| DecimalNumberParsingError::InvalidNumber(s.to_owned()))?; 29 | fraction = fraction 30 | .checked_mul( 31 | pref_const 32 | .checked_div(10u128.checked_pow(len).ok_or_else(|| { 33 | DecimalNumberParsingError::LongFractional(fractional.to_owned()) 34 | })?) 35 | .filter(|n| *n != 0u128) 36 | .ok_or_else(|| { 37 | DecimalNumberParsingError::LongFractional(fractional.to_owned()) 38 | })?, 39 | ) 40 | .ok_or_else(|| DecimalNumberParsingError::LongFractional(fractional.to_owned()))?; 41 | (int, fraction) 42 | } else { 43 | let int: u128 = s 44 | .parse() 45 | .map_err(|_| DecimalNumberParsingError::InvalidNumber(s.to_owned()))?; 46 | (int, 0) 47 | }; 48 | let result = fraction 49 | .checked_add( 50 | int.checked_mul(pref_const) 51 | .ok_or_else(|| DecimalNumberParsingError::LongWhole(int.to_string()))?, 52 | ) 53 | .ok_or_else(|| DecimalNumberParsingError::LongWhole(int.to_string()))?; 54 | Ok(result) 55 | } 56 | 57 | pub fn is_critical_blocks_error( 58 | err: &near_jsonrpc_client::errors::JsonRpcError< 59 | near_jsonrpc_primitives::types::blocks::RpcBlockError, 60 | >, 61 | ) -> bool { 62 | is_critical_json_rpc_error(err, |err| match err { 63 | near_jsonrpc_client::methods::block::RpcBlockError::UnknownBlock { .. } 64 | | near_jsonrpc_client::methods::block::RpcBlockError::NotSyncedYet 65 | | near_jsonrpc_client::methods::block::RpcBlockError::InternalError { .. } => true, 66 | }) 67 | } 68 | 69 | pub fn is_critical_validator_error( 70 | err: &near_jsonrpc_client::errors::JsonRpcError< 71 | near_jsonrpc_primitives::types::validator::RpcValidatorError, 72 | >, 73 | ) -> bool { 74 | is_critical_json_rpc_error(err, |err| match err { 75 | near_jsonrpc_primitives::types::validator::RpcValidatorError::UnknownEpoch 76 | | near_jsonrpc_primitives::types::validator::RpcValidatorError::ValidatorInfoUnavailable 77 | | near_jsonrpc_primitives::types::validator::RpcValidatorError::InternalError { .. } => { 78 | true 79 | } 80 | }) 81 | } 82 | 83 | pub fn is_critical_query_error( 84 | err: &near_jsonrpc_client::errors::JsonRpcError< 85 | near_jsonrpc_primitives::types::query::RpcQueryError, 86 | >, 87 | ) -> bool { 88 | is_critical_json_rpc_error(err, |err| match err { 89 | near_jsonrpc_primitives::types::query::RpcQueryError::NoSyncedBlocks 90 | | near_jsonrpc_primitives::types::query::RpcQueryError::UnavailableShard { .. } 91 | | near_jsonrpc_primitives::types::query::RpcQueryError::GarbageCollectedBlock { .. } 92 | | near_jsonrpc_primitives::types::query::RpcQueryError::UnknownBlock { .. } 93 | | near_jsonrpc_primitives::types::query::RpcQueryError::InvalidAccount { .. } 94 | | near_jsonrpc_primitives::types::query::RpcQueryError::UnknownAccount { .. } 95 | | near_jsonrpc_primitives::types::query::RpcQueryError::NoContractCode { .. } 96 | | near_jsonrpc_primitives::types::query::RpcQueryError::TooLargeContractState { .. } 97 | | near_jsonrpc_primitives::types::query::RpcQueryError::UnknownAccessKey { .. } 98 | | near_jsonrpc_primitives::types::query::RpcQueryError::ContractExecutionError { .. } 99 | | near_jsonrpc_primitives::types::query::RpcQueryError::InternalError { .. } => true, 100 | }) 101 | } 102 | 103 | pub fn is_critical_transaction_error( 104 | err: &near_jsonrpc_client::errors::JsonRpcError< 105 | near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError, 106 | >, 107 | ) -> bool { 108 | is_critical_json_rpc_error(err, |err| { 109 | match err { 110 | near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::TimeoutError => { 111 | false 112 | } 113 | near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::InvalidTransaction { .. } | 114 | near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::DoesNotTrackShard | 115 | near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::RequestRouted{..} | 116 | near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::UnknownTransaction{..} | 117 | near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::InternalError{..} => { 118 | true 119 | } 120 | } 121 | }) 122 | } 123 | 124 | fn is_critical_json_rpc_error( 125 | err: &near_jsonrpc_client::errors::JsonRpcError, 126 | is_critical_t: impl Fn(&T) -> bool, 127 | ) -> bool { 128 | match err { 129 | near_jsonrpc_client::errors::JsonRpcError::TransportError(_rpc_transport_error) => { 130 | false 131 | } 132 | near_jsonrpc_client::errors::JsonRpcError::ServerError(rpc_server_error) => match rpc_server_error { 133 | near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(rpc_transaction_error) => is_critical_t(rpc_transaction_error), 134 | near_jsonrpc_client::errors::JsonRpcServerError::RequestValidationError(..) | 135 | near_jsonrpc_client::errors::JsonRpcServerError::NonContextualError(..) => { 136 | true 137 | } 138 | near_jsonrpc_client::errors::JsonRpcServerError::InternalError{ .. } => { 139 | false 140 | } 141 | near_jsonrpc_client::errors::JsonRpcServerError::ResponseStatusError(json_rpc_server_response_status_error) => match json_rpc_server_response_status_error { 142 | near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::Unauthorized | 143 | near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::Unexpected{..} | 144 | near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::BadRequest => { 145 | true 146 | } 147 | near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::TimeoutError | 148 | near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::ServiceUnavailable | 149 | near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::TooManyRequests => { 150 | false 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | #[cfg(test)] 158 | mod tests { 159 | use super::*; 160 | 161 | const TEST: [(u128, &str, u128); 6] = [ 162 | (129_380_000_001_u128, "129.380000001", 10u128.pow(9)), 163 | ( 164 | 12_938_000_000_100_000_000_u128, 165 | "12938000000.1", 166 | 10u128.pow(9), 167 | ), 168 | (129_380_000_001_u128, "0.129380000001", 10u128.pow(12)), 169 | (129_380_000_001_000_u128, "129.380000001000", 10u128.pow(12)), 170 | ( 171 | 9_488_129_380_000_001_u128, 172 | "9488.129380000001", 173 | 10u128.pow(12), 174 | ), 175 | (129_380_000_001_u128, "00.129380000001", 10u128.pow(12)), 176 | ]; 177 | 178 | #[test] 179 | fn parse_test() { 180 | for (expected_value, str_value, precision) in TEST { 181 | let parsed_value = parse_decimal_number(str_value, precision).unwrap(); 182 | assert_eq!(parsed_value, expected_value) 183 | } 184 | } 185 | 186 | #[test] 187 | fn test_long_fraction() { 188 | let data = "1.23456"; 189 | let prefix = 10000u128; 190 | assert_eq!( 191 | parse_decimal_number(data, prefix), 192 | Err(DecimalNumberParsingError::LongFractional(23456.to_string())) 193 | ); 194 | } 195 | 196 | #[test] 197 | fn invalid_number_whole() { 198 | let num = "1h4.7859"; 199 | let prefix: u128 = 10000; 200 | assert_eq!( 201 | parse_decimal_number(num, prefix), 202 | Err(DecimalNumberParsingError::InvalidNumber( 203 | "1h4.7859".to_owned() 204 | )) 205 | ); 206 | } 207 | #[test] 208 | fn invalid_number_fraction() { 209 | let num = "14.785h9"; 210 | let prefix: u128 = 10000; 211 | assert_eq!( 212 | parse_decimal_number(num, prefix), 213 | Err(DecimalNumberParsingError::InvalidNumber( 214 | "14.785h9".to_owned() 215 | )) 216 | ); 217 | } 218 | 219 | #[test] 220 | fn max_long_fraction() { 221 | let max_data = 10u128.pow(17) + 1; 222 | let data = "1.".to_string() + max_data.to_string().as_str(); 223 | let prefix = 10u128.pow(17); 224 | assert_eq!( 225 | parse_decimal_number(data.as_str(), prefix), 226 | Err(DecimalNumberParsingError::LongFractional( 227 | max_data.to_string() 228 | )) 229 | ); 230 | } 231 | 232 | #[test] 233 | fn parse_u128_error_test() { 234 | let test_data = u128::MAX.to_string(); 235 | let gas = parse_decimal_number(&test_data, 10u128.pow(9)); 236 | assert_eq!( 237 | gas, 238 | Err(DecimalNumberParsingError::LongWhole(u128::MAX.to_string())) 239 | ); 240 | } 241 | 242 | #[test] 243 | fn test() { 244 | let data = "1.000000000000000000000000000000000000001"; 245 | let prefix = 100u128; 246 | assert_eq!( 247 | parse_decimal_number(data, prefix), 248 | Err(DecimalNumberParsingError::LongFractional( 249 | "000000000000000000000000000000000000001".to_string() 250 | )) 251 | ); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use near_jsonrpc_client::JsonRpcClient; 2 | 3 | use crate::errors::RetryError; 4 | 5 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 6 | /// Specifies the retry strategy for RPC endpoint requests. 7 | pub enum RetryMethod { 8 | /// Exponential backoff strategy with configurable initial delay and multiplication factor. 9 | /// The delay is calculated as: `initial_sleep * factor^retry_number` 10 | ExponentialBackoff { 11 | /// The initial delay duration before the first retry 12 | initial_sleep: std::time::Duration, 13 | /// The multiplication factor for calculating subsequent delays 14 | factor: u8, 15 | }, 16 | /// Fixed delay strategy with constant sleep duration 17 | Fixed { 18 | /// The constant delay duration between retries 19 | sleep: std::time::Duration, 20 | }, 21 | } 22 | 23 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 24 | /// Configuration for a [NEAR RPC](https://docs.near.org/api/rpc/providers) endpoint with retry and backoff settings. 25 | pub struct RPCEndpoint { 26 | /// The URL of the RPC endpoint 27 | pub url: url::Url, 28 | /// Optional API key for authenticated requests 29 | pub api_key: Option, 30 | /// Number of consecutive failures to move on to the next endpoint. 31 | pub retries: u8, 32 | /// The retry method to use 33 | pub retry_method: RetryMethod, 34 | } 35 | 36 | impl RPCEndpoint { 37 | /// Constructs a new RPC endpoint configuration with default settings. 38 | /// 39 | /// The default retry method is `ExponentialBackoff` with an initial sleep of 10ms and a factor of 2. 40 | /// The delays will be 10ms, 20ms, 40ms, 80ms, 160ms. 41 | pub const fn new(url: url::Url) -> Self { 42 | Self { 43 | url, 44 | api_key: None, 45 | retries: 5, 46 | // 10ms, 20ms, 40ms, 80ms, 160ms 47 | retry_method: RetryMethod::ExponentialBackoff { 48 | initial_sleep: std::time::Duration::from_millis(10), 49 | factor: 2, 50 | }, 51 | } 52 | } 53 | 54 | /// Constructs default mainnet configuration. 55 | pub fn mainnet() -> Self { 56 | Self::new("https://archival-rpc.mainnet.near.org".parse().unwrap()) 57 | } 58 | 59 | /// Constructs default testnet configuration. 60 | pub fn testnet() -> Self { 61 | Self::new("https://archival-rpc.testnet.near.org".parse().unwrap()) 62 | } 63 | 64 | /// Set API key for the endpoint. 65 | pub fn with_api_key(mut self, api_key: crate::types::ApiKey) -> Self { 66 | self.api_key = Some(api_key); 67 | self 68 | } 69 | 70 | /// Set number of retries for the endpoint before moving on to the next one. 71 | pub const fn with_retries(mut self, retries: u8) -> Self { 72 | self.retries = retries; 73 | self 74 | } 75 | 76 | pub const fn with_retry_method(mut self, retry_method: RetryMethod) -> Self { 77 | self.retry_method = retry_method; 78 | self 79 | } 80 | 81 | pub fn get_sleep_duration(&self, retry: usize) -> std::time::Duration { 82 | match self.retry_method { 83 | RetryMethod::ExponentialBackoff { 84 | initial_sleep, 85 | factor, 86 | } => initial_sleep * ((factor as u32).pow(retry as u32)), 87 | RetryMethod::Fixed { sleep } => sleep, 88 | } 89 | } 90 | } 91 | 92 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 93 | /// Configuration for a NEAR network including RPC endpoints and network-specific settings. 94 | /// 95 | /// # Multiple RPC endpoints 96 | /// 97 | /// This struct is used to configure multiple RPC endpoints for a NEAR network. 98 | /// It allows for failover between endpoints in case of a failure. 99 | /// 100 | /// 101 | /// ## Example 102 | /// ```rust,no_run 103 | /// use near_api::*; 104 | /// 105 | /// # async fn example() -> Result<(), Box> { 106 | /// let config = NetworkConfig { 107 | /// rpc_endpoints: vec![RPCEndpoint::mainnet(), RPCEndpoint::new("https://near.lava.build".parse()?)], 108 | /// ..NetworkConfig::mainnet() 109 | /// }; 110 | /// # Ok(()) 111 | /// # } 112 | /// ``` 113 | pub struct NetworkConfig { 114 | /// Human readable name of the network (e.g. "mainnet", "testnet") 115 | pub network_name: String, 116 | /// List of [RPC endpoints](https://docs.near.org/api/rpc/providers) to use with failover 117 | pub rpc_endpoints: Vec, 118 | /// Account ID used for [linkdrop functionality](https://docs.near.org/build/primitives/linkdrop) 119 | pub linkdrop_account_id: Option, 120 | /// Account ID of the [NEAR Social contract](https://docs.near.org/social/contract) 121 | pub near_social_db_contract_account_id: Option, 122 | /// URL of the network's faucet service 123 | pub faucet_url: Option, 124 | /// URL for the [meta transaction relayer](https://docs.near.org/concepts/abstraction/relayers) service 125 | pub meta_transaction_relayer_url: Option, 126 | /// URL for the [fastnear](https://docs.near.org/tools/ecosystem-apis/fastnear-api) service. 127 | /// 128 | /// Currently, unused. See [#30](https://github.com/near/near-api-rs/issues/30) 129 | pub fastnear_url: Option, 130 | /// Account ID of the [staking pools factory](https://github.com/NearSocial/social-db) 131 | pub staking_pools_factory_account_id: Option, 132 | } 133 | 134 | impl NetworkConfig { 135 | /// Constructs default mainnet configuration. 136 | pub fn mainnet() -> Self { 137 | Self { 138 | network_name: "mainnet".to_string(), 139 | rpc_endpoints: vec![RPCEndpoint::mainnet()], 140 | linkdrop_account_id: Some("near".parse().unwrap()), 141 | near_social_db_contract_account_id: Some("social.near".parse().unwrap()), 142 | faucet_url: None, 143 | meta_transaction_relayer_url: None, 144 | fastnear_url: Some("https://api.fastnear.com/".parse().unwrap()), 145 | staking_pools_factory_account_id: Some("poolv1.near".parse().unwrap()), 146 | } 147 | } 148 | 149 | /// Constructs default testnet configuration. 150 | pub fn testnet() -> Self { 151 | Self { 152 | network_name: "testnet".to_string(), 153 | rpc_endpoints: vec![RPCEndpoint::testnet()], 154 | linkdrop_account_id: Some("testnet".parse().unwrap()), 155 | near_social_db_contract_account_id: Some("v1.social08.testnet".parse().unwrap()), 156 | faucet_url: Some("https://helper.nearprotocol.com/account".parse().unwrap()), 157 | meta_transaction_relayer_url: None, 158 | fastnear_url: None, 159 | staking_pools_factory_account_id: Some("pool.f863973.m0".parse().unwrap()), 160 | } 161 | } 162 | 163 | pub(crate) fn json_rpc_client(&self, index: usize) -> near_jsonrpc_client::JsonRpcClient { 164 | let rpc_endpoint = &self.rpc_endpoints[index]; 165 | let mut json_rpc_client = 166 | near_jsonrpc_client::JsonRpcClient::connect(rpc_endpoint.url.as_ref()); 167 | if let Some(rpc_api_key) = &rpc_endpoint.api_key { 168 | json_rpc_client = 169 | json_rpc_client.header(near_jsonrpc_client::auth::ApiKey::from(rpc_api_key.clone())) 170 | }; 171 | json_rpc_client 172 | } 173 | } 174 | 175 | #[cfg(feature = "workspaces")] 176 | impl From> for NetworkConfig { 177 | fn from(network: near_workspaces::Worker) -> Self { 178 | use near_workspaces::network::NetworkInfo; 179 | 180 | let info = network.info(); 181 | Self { 182 | network_name: info.name.clone(), 183 | rpc_endpoints: vec![RPCEndpoint::new(info.rpc_url.clone())], 184 | linkdrop_account_id: None, 185 | near_social_db_contract_account_id: None, 186 | faucet_url: None, 187 | fastnear_url: None, 188 | meta_transaction_relayer_url: None, 189 | staking_pools_factory_account_id: None, 190 | } 191 | } 192 | } 193 | 194 | #[derive(Debug)] 195 | /// Represents the possible outcomes of a retry-able operation. 196 | pub enum RetryResponse { 197 | /// Operation succeeded with result R 198 | Ok(R), 199 | /// Operation failed with error E, should be retried 200 | Retry(E), 201 | /// Operation failed with critical error E, should not be retried 202 | Critical(E), 203 | } 204 | 205 | impl From> for RetryResponse { 206 | fn from(value: Result) -> Self { 207 | match value { 208 | Ok(value) => Self::Ok(value), 209 | Err(value) => Self::Retry(value), 210 | } 211 | } 212 | } 213 | 214 | /// Retry a task with exponential backoff and failover. 215 | /// 216 | /// # Arguments 217 | /// * `network` - The network configuration to use for the retry-able operation. 218 | /// * `task` - The task to retry. 219 | pub async fn retry(network: NetworkConfig, mut task: F) -> Result> 220 | where 221 | F: FnMut(JsonRpcClient) -> T + Send, 222 | T: core::future::Future> + Send, 223 | T::Output: Send, 224 | E: Send, 225 | { 226 | if network.rpc_endpoints.is_empty() { 227 | return Err(RetryError::NoRpcEndpoints); 228 | } 229 | 230 | let mut last_error = None; 231 | for (index, endpoint) in network.rpc_endpoints.iter().enumerate() { 232 | let client = network.json_rpc_client(index); 233 | for retry in 0..endpoint.retries { 234 | let result = task(client.clone()).await; 235 | match result { 236 | RetryResponse::Ok(result) => return Ok(result), 237 | RetryResponse::Retry(error) => { 238 | last_error = Some(error); 239 | tokio::time::sleep(endpoint.get_sleep_duration(retry as usize)).await; 240 | } 241 | RetryResponse::Critical(result) => return Err(RetryError::Critical(result)), 242 | } 243 | } 244 | } 245 | Err(RetryError::RetriesExhausted(last_error.expect( 246 | "Logic error: last_error should be Some when all retries are exhausted", 247 | ))) 248 | } 249 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use near_jsonrpc_client::{ 2 | errors::JsonRpcError, 3 | methods::{query::RpcQueryRequest, tx::RpcTransactionError, RpcMethod}, 4 | }; 5 | use near_jsonrpc_primitives::types::query::QueryResponseKind; 6 | 7 | #[derive(thiserror::Error, Debug)] 8 | pub enum QueryCreationError { 9 | #[error("Staking pool factory account ID is not defined in the network config")] 10 | StakingPoolFactoryNotDefined, 11 | } 12 | 13 | #[derive(thiserror::Error, Debug)] 14 | pub enum QueryError 15 | where 16 | Method::Error: std::fmt::Debug + std::fmt::Display + 'static, 17 | { 18 | #[error(transparent)] 19 | QueryCreationError(#[from] QueryCreationError), 20 | #[error("Unexpected response kind: expected {expected} type, but got {got:?}")] 21 | UnexpectedResponse { 22 | expected: &'static str, 23 | // Boxed to avoid large error type 24 | got: Box, 25 | }, 26 | #[error("Failed to deserialize response: {0}")] 27 | DeserializeError(#[from] serde_json::Error), 28 | #[error("Query error: {0}")] 29 | JsonRpcError(Box>>), 30 | #[error("Internal error: failed to get response. Please submit a bug ticket")] 31 | InternalErrorNoResponse, 32 | } 33 | 34 | impl From>> for QueryError 35 | where 36 | Method::Error: std::fmt::Debug + std::fmt::Display + 'static, 37 | { 38 | fn from(err: RetryError>) -> Self { 39 | Self::JsonRpcError(Box::new(err)) 40 | } 41 | } 42 | 43 | #[derive(thiserror::Error, Debug)] 44 | pub enum MetaSignError { 45 | // near_primitives::action::delegate::IsDelegateAction is private, so we redefined it here 46 | #[error("Attempted to construct NonDelegateAction from Action::Delegate")] 47 | DelegateActionIsNotSupported, 48 | 49 | #[error(transparent)] 50 | SignerError(#[from] SignerError), 51 | } 52 | 53 | #[derive(thiserror::Error, Debug)] 54 | pub enum SignerError { 55 | #[error("Public key is not available")] 56 | PublicKeyIsNotAvailable, 57 | #[error("Secret key is not available")] 58 | SecretKeyIsNotAvailable, 59 | #[error("Failed to fetch nonce: {0}")] 60 | FetchNonceError(#[from] QueryError), 61 | #[error("IO error: {0}")] 62 | IO(#[from] std::io::Error), 63 | 64 | #[cfg(feature = "ledger")] 65 | #[error(transparent)] 66 | LedgerError(#[from] LedgerError), 67 | } 68 | 69 | #[derive(thiserror::Error, Debug)] 70 | pub enum SecretError { 71 | #[error("Failed to process seed phrase: {0}")] 72 | BIP39Error(#[from] bip39::Error), 73 | #[error("Failed to derive key from seed phrase: Invalid Index")] 74 | DeriveKeyInvalidIndex, 75 | } 76 | 77 | #[derive(thiserror::Error, Debug)] 78 | pub enum AccessKeyFileError { 79 | #[error("Failed to read access key file: {0}")] 80 | ReadError(#[from] std::io::Error), 81 | #[error("Failed to parse access key file: {0}")] 82 | ParseError(#[from] serde_json::Error), 83 | #[error(transparent)] 84 | SecretError(#[from] SecretError), 85 | #[error("Public key is not linked to the private key")] 86 | PrivatePublicKeyMismatch, 87 | } 88 | 89 | #[cfg(feature = "keystore")] 90 | #[derive(thiserror::Error, Debug)] 91 | pub enum KeyStoreError { 92 | #[error(transparent)] 93 | Keystore(#[from] keyring::Error), 94 | #[error("Failed to query account keys: {0}")] 95 | QueryError(#[from] QueryError), 96 | #[error("Failed to parse access key file: {0}")] 97 | ParseError(#[from] serde_json::Error), 98 | #[error(transparent)] 99 | SecretError(#[from] SecretError), 100 | #[error("Task execution error: {0}")] 101 | TaskExecutionError(#[from] tokio::task::JoinError), 102 | } 103 | 104 | #[cfg(feature = "ledger")] 105 | #[derive(thiserror::Error, Debug)] 106 | pub enum LedgerError { 107 | #[error( 108 | "Buffer overflow on Ledger device occurred. \ 109 | Transaction is too large for signature. \ 110 | This is resolved in https://github.com/dj8yfo/app-near-rs . \ 111 | The status is tracked in `About` section." 112 | )] 113 | BufferOverflow, 114 | #[error("Ledger device error: {0:?}")] 115 | LedgerError(near_ledger::NEARLedgerError), 116 | #[error("IO error: {0}")] 117 | IO(#[from] std::io::Error), 118 | #[error("Task execution error: {0}")] 119 | TaskExecutionError(#[from] tokio::task::JoinError), 120 | #[error("Signature is not expected to fail on deserialization: {0}")] 121 | SignatureDeserializationError(#[from] near_crypto::ParseSignatureError), 122 | } 123 | 124 | #[cfg(feature = "ledger")] 125 | impl From for LedgerError { 126 | fn from(err: near_ledger::NEARLedgerError) -> Self { 127 | const SW_BUFFER_OVERFLOW: &str = "0x6990"; 128 | 129 | match err { 130 | near_ledger::NEARLedgerError::APDUExchangeError(msg) 131 | if msg.contains(SW_BUFFER_OVERFLOW) => 132 | { 133 | Self::BufferOverflow 134 | } 135 | near_ledger_error => Self::LedgerError(near_ledger_error), 136 | } 137 | } 138 | } 139 | 140 | #[derive(thiserror::Error, Debug)] 141 | pub enum SignedDelegateActionError { 142 | #[error("Parsing of signed delegate action failed due to base64 sequence being invalid")] 143 | Base64DecodingError, 144 | #[error("Delegate action could not be deserialized from borsh: {0}")] 145 | BorshError(#[from] std::io::Error), 146 | } 147 | 148 | #[derive(thiserror::Error, Debug)] 149 | pub enum SecretBuilderError { 150 | #[error("Public key is not available")] 151 | PublicKeyIsNotAvailable, 152 | #[error(transparent)] 153 | SecretError(#[from] SecretError), 154 | #[error(transparent)] 155 | IO(#[from] std::io::Error), 156 | #[error(transparent)] 157 | CallbackError(E), 158 | } 159 | 160 | #[derive(thiserror::Error, Debug)] 161 | pub enum BuilderError { 162 | #[error("Incorrect arguments: {0}")] 163 | IncorrectArguments(#[from] serde_json::Error), 164 | } 165 | 166 | #[derive(thiserror::Error, Debug)] 167 | pub enum AccountCreationError { 168 | #[error(transparent)] 169 | BuilderError(#[from] BuilderError), 170 | 171 | #[error("Top-level account is not allowed")] 172 | TopLevelAccountIsNotAllowed, 173 | 174 | #[error("Linkdrop is not defined in the network config")] 175 | LinkdropIsNotDefined, 176 | 177 | #[error("Account should be created as a sub-account of the signer or linkdrop account")] 178 | AccountShouldBeSubAccountOfSignerOrLinkdrop, 179 | } 180 | 181 | #[derive(thiserror::Error, Debug)] 182 | pub enum FaucetError { 183 | #[error("The <{0}> network config does not have a defined faucet (helper service) that can sponsor the creation of an account.")] 184 | FaucetIsNotDefined(String), 185 | #[error("Failed to send message: {0}")] 186 | SendError(#[from] reqwest::Error), 187 | } 188 | 189 | #[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] 190 | pub enum DecimalNumberParsingError { 191 | #[error("Invalid number: {0}")] 192 | InvalidNumber(String), 193 | #[error("Too long whole part: {0}")] 194 | LongWhole(String), 195 | #[error("Too long fractional part: {0}")] 196 | LongFractional(String), 197 | } 198 | 199 | #[derive(thiserror::Error, Debug)] 200 | pub enum RetryError { 201 | #[error("No RPC endpoints are defined in the network config")] 202 | NoRpcEndpoints, 203 | #[error("Request failed. Retries exhausted. Last error: {0}")] 204 | RetriesExhausted(E), 205 | #[error("Critical error: {0}")] 206 | Critical(E), 207 | } 208 | 209 | #[derive(thiserror::Error, Debug)] 210 | pub enum ExecuteTransactionError { 211 | #[error("Transaction validation error: {0}")] 212 | ValidationError(#[from] ValidationError), 213 | #[error("Transaction signing error: {0}")] 214 | SignerError(#[from] SignerError), 215 | #[error("Meta-signing error: {0}")] 216 | MetaSignError(#[from] MetaSignError), 217 | #[error("Pre-query error: {0}")] 218 | PreQueryError(#[from] QueryError), 219 | #[error("Transaction error: {0}")] 220 | TransactionError(#[from] RetryError>), 221 | #[deprecated(since = "0.2.1", note = "unused")] 222 | #[error("Transaction error: {0}")] 223 | CriticalTransactionError(JsonRpcError), 224 | #[error(transparent)] 225 | NonEmptyVecError(#[from] NonEmptyVecError), 226 | } 227 | 228 | #[derive(thiserror::Error, Debug)] 229 | pub enum ExecuteMetaTransactionsError { 230 | #[error("Transaction validation error: {0}")] 231 | ValidationError(#[from] ValidationError), 232 | #[error("Meta-signing error: {0}")] 233 | SignError(#[from] MetaSignError), 234 | #[error("Pre-query error: {0}")] 235 | PreQueryError(#[from] QueryError), 236 | 237 | #[error("Relayer is not defined in the network config")] 238 | RelayerIsNotDefined, 239 | 240 | #[error("Failed to send meta-transaction: {0}")] 241 | SendError(#[from] reqwest::Error), 242 | 243 | #[error(transparent)] 244 | NonEmptyVecError(#[from] NonEmptyVecError), 245 | } 246 | 247 | #[derive(thiserror::Error, Debug)] 248 | pub enum FTValidatorError { 249 | #[error("Metadata is not provided")] 250 | NoMetadata, 251 | #[error("Decimals mismatch: expected {expected}, got {got}")] 252 | DecimalsMismatch { expected: u8, got: u8 }, 253 | #[error("Storage deposit is needed")] 254 | StorageDepositNeeded, 255 | } 256 | 257 | #[derive(thiserror::Error, Debug)] 258 | pub enum FastNearError { 259 | #[error("FastNear URL is not defined in the network config")] 260 | FastNearUrlIsNotDefined, 261 | #[error("Failed to send request: {0}")] 262 | SendError(#[from] reqwest::Error), 263 | #[error("Url parsing error: {0}")] 264 | UrlParseError(#[from] url::ParseError), 265 | } 266 | 267 | //TODO: it's better to have a separate errors, but for now it would be aggregated here 268 | #[derive(thiserror::Error, Debug)] 269 | pub enum ValidationError { 270 | #[error("Query error: {0}")] 271 | QueryError(#[from] QueryError), 272 | 273 | #[error("Query creation error: {0}")] 274 | QueryBuilderError(#[from] BuilderError), 275 | 276 | #[error("FT Validation Error: {0}")] 277 | FTValidatorError(#[from] FTValidatorError), 278 | 279 | #[error("Account creation error: {0}")] 280 | AccountCreationError(#[from] AccountCreationError), 281 | } 282 | 283 | #[derive(thiserror::Error, Debug)] 284 | pub enum MultiTransactionError { 285 | #[error(transparent)] 286 | NonEmptyVecError(#[from] NonEmptyVecError), 287 | 288 | #[error(transparent)] 289 | SignerError(#[from] SignerError), 290 | #[error("Duplicate signer")] 291 | DuplicateSigner, 292 | 293 | #[error(transparent)] 294 | SignedTransactionError(#[from] ExecuteTransactionError), 295 | 296 | #[error("Failed to send meta-transaction: {0}")] 297 | MetaTransactionError(#[from] ExecuteMetaTransactionsError), 298 | } 299 | 300 | #[derive(thiserror::Error, Debug)] 301 | pub enum NonEmptyVecError { 302 | #[error("Vector is empty")] 303 | EmptyVector, 304 | } 305 | 306 | #[derive(thiserror::Error, Debug)] 307 | pub enum CryptoHashError { 308 | #[error(transparent)] 309 | Base58DecodeError(#[from] bs58::decode::Error), 310 | #[error("Incorrect hash length (expected 32, but {0} was given)")] 311 | IncorrectHashLength(usize), 312 | } 313 | -------------------------------------------------------------------------------- /src/fastnear.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use near_primitives::types::AccountId; 4 | use serde::de::DeserializeOwned; 5 | 6 | use crate::errors::FastNearError; 7 | 8 | #[derive(Debug, serde::Deserialize)] 9 | pub struct StakingPool { 10 | pool_id: near_primitives::types::AccountId, 11 | } 12 | 13 | #[derive(Debug, serde::Deserialize)] 14 | pub struct StakingResponse { 15 | pools: Vec, 16 | } 17 | 18 | pub struct FastNearBuilder { 19 | query: String, 20 | post_process: Box PostProcessed + Send + Sync>, 21 | _response: std::marker::PhantomData, 22 | } 23 | 24 | impl FastNearBuilder { 25 | pub fn new(query: String) -> Self { 26 | Self { 27 | query, 28 | post_process: Box::new(|response| response), 29 | _response: Default::default(), 30 | } 31 | } 32 | } 33 | 34 | impl FastNearBuilder 35 | where 36 | T: DeserializeOwned + Send + Sync, 37 | { 38 | pub fn map(query: String, func: F) -> Self 39 | where 40 | F: Fn(T) -> PostProcessed + Send + Sync + 'static, 41 | { 42 | Self { 43 | query, 44 | post_process: Box::new(func), 45 | _response: Default::default(), 46 | } 47 | } 48 | 49 | pub async fn fetch_from_url(self, url: url::Url) -> Result { 50 | let request = reqwest::get(url.join(&self.query)?).await?; 51 | Ok((self.post_process)(request.json().await?)) 52 | } 53 | 54 | pub async fn fetch_from_mainnet(self) -> Result { 55 | match crate::config::NetworkConfig::mainnet().fastnear_url { 56 | Some(url) => self.fetch_from_url(url).await, 57 | None => Err(FastNearError::FastNearUrlIsNotDefined), 58 | } 59 | } 60 | } 61 | 62 | #[derive(Clone, Debug)] 63 | pub struct FastNear {} 64 | 65 | impl FastNear { 66 | pub async fn pools_delegated_by( 67 | &self, 68 | account_id: &AccountId, 69 | ) -> Result>, FastNearError> { 70 | let query_builder = FastNearBuilder::map( 71 | format!("v1/account/{}/staking", account_id), 72 | |response: StakingResponse| { 73 | response 74 | .pools 75 | .into_iter() 76 | .map(|pool| pool.pool_id) 77 | .collect() 78 | }, 79 | ); 80 | 81 | Ok(query_builder) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Rust library for interacting with the NEAR Protocol blockchain 2 | //! 3 | //! This crate provides a high-level API for interacting with NEAR Protocol, including: 4 | //! - [Account management and creation](Account) 5 | //! - [Contract deployment and interaction with it](Contract) 6 | //! - [Token operations](Tokens) ([`NEAR`](https://docs.near.org/concepts/basics/tokens), [`FT`](https://docs.near.org/build/primitives/ft), [`NFT`](https://docs.near.org/build/primitives/nft)) 7 | //! - [Storage management](StorageDeposit) 8 | //! - [Staking operations](Staking) 9 | //! - [Custom transaction building and signing](Transaction) 10 | //! - [Querying the chain data](Chain) 11 | //! - [Several ways to sign the transaction](signer) 12 | //! - Account nonce caching and access-key pooling mechanisms to speed up the transaction processing. 13 | //! - Support for backup RPC endpoints 14 | //! 15 | //! # Example 16 | //! In this example, we use Bob account with a predefined seed phrase to create Alice account and pre-fund it with 1 `NEAR`. 17 | //! ```rust,no_run 18 | //! use near_api::{*, signer::generate_secret_key}; 19 | //! use std::str::FromStr; 20 | //! 21 | //! # async fn example() -> Result<(), Box> { 22 | //! // Initialize network configuration 23 | //! let bob = AccountId::from_str("bob.testnet")?; 24 | //! let bob_seed_phrase = "lucky barrel fall come bottom can rib join rough around subway cloth "; 25 | //! 26 | //! // Fetch NEAR balance 27 | //! let _bob_balance = Tokens::account(bob.clone()) 28 | //! .near_balance() 29 | //! .fetch_from_testnet() 30 | //! .await?; 31 | //! 32 | //! // Create an account instance 33 | //! let signer = Signer::new(Signer::from_seed_phrase(bob_seed_phrase, None)?)?; 34 | //! let alice_secret_key = generate_secret_key()?; 35 | //! Account::create_account(AccountId::from_str("alice.testnet")?) 36 | //! .fund_myself(bob.clone(), NearToken::from_near(1)) 37 | //! .public_key(alice_secret_key.public_key())? 38 | //! .with_signer(signer) 39 | //! .send_to_testnet() 40 | //! .await?; 41 | //! # Ok(()) 42 | //! # } 43 | //! ``` 44 | //! 45 | //! # Features 46 | //! - `ledger`: Enables hardware wallet support 47 | //! - `keystore`: Enables system keychain integration 48 | //! - `workspaces`: Enables integration with near-workspaces for testing 49 | 50 | mod account; 51 | mod chain; 52 | mod config; 53 | mod contract; 54 | mod stake; 55 | mod storage; 56 | mod tokens; 57 | mod transactions; 58 | // TODO: to be honest, there is almost nothing in this file 59 | // we should maybe integrate with them more tightly 60 | // for now, i comment it out 61 | // mod fastnear; 62 | 63 | mod common; 64 | 65 | pub mod errors; 66 | pub mod signer; 67 | pub mod types; 68 | 69 | pub use crate::{ 70 | account::Account, 71 | chain::Chain, 72 | config::{NetworkConfig, RPCEndpoint, RetryMethod}, 73 | contract::Contract, 74 | signer::{Signer, SignerTrait}, 75 | stake::{Delegation, Staking}, 76 | storage::StorageDeposit, 77 | tokens::Tokens, 78 | transactions::Transaction, 79 | types::{ 80 | reference::{EpochReference, Reference}, 81 | tokens::{FTBalance, USDT_BALANCE, W_NEAR_BALANCE}, 82 | Data, 83 | }, 84 | }; 85 | 86 | pub mod advanced { 87 | pub use crate::common::query::*; 88 | pub use crate::common::send::*; 89 | } 90 | 91 | pub use near_primitives; 92 | 93 | pub use near_account_id::AccountId; 94 | pub use near_gas::NearGas; 95 | pub use near_token::NearToken; 96 | -------------------------------------------------------------------------------- /src/signer/keystore.rs: -------------------------------------------------------------------------------- 1 | use futures::future::join_all; 2 | use near_crypto::{PublicKey, SecretKey}; 3 | use near_primitives::{types::AccountId, views::AccessKeyPermissionView}; 4 | use tracing::{debug, info, instrument, trace, warn}; 5 | 6 | use crate::{ 7 | config::NetworkConfig, 8 | errors::{KeyStoreError, SignerError}, 9 | }; 10 | 11 | use super::{AccountKeyPair, SignerTrait}; 12 | 13 | const KEYSTORE_SIGNER_TARGET: &str = "near_api::signer::keystore"; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct KeystoreSigner { 17 | potential_pubkeys: Vec, 18 | } 19 | 20 | #[async_trait::async_trait] 21 | impl SignerTrait for KeystoreSigner { 22 | #[instrument(skip(self))] 23 | async fn get_secret_key( 24 | &self, 25 | signer_id: &AccountId, 26 | public_key: &PublicKey, 27 | ) -> Result { 28 | debug!(target: KEYSTORE_SIGNER_TARGET, "Searching for matching public key"); 29 | self.potential_pubkeys 30 | .iter() 31 | .find(|key| *key == public_key) 32 | .ok_or(SignerError::PublicKeyIsNotAvailable)?; 33 | 34 | info!(target: KEYSTORE_SIGNER_TARGET, "Retrieving secret key"); 35 | // TODO: fix this. Well the search is a bit suboptimal, but it's not a big deal for now 36 | let secret = 37 | if let Ok(secret) = Self::get_secret_key(signer_id, public_key, "mainnet").await { 38 | secret 39 | } else { 40 | Self::get_secret_key(signer_id, public_key, "testnet") 41 | .await 42 | .map_err(|_| SignerError::SecretKeyIsNotAvailable)? 43 | }; 44 | 45 | info!(target: KEYSTORE_SIGNER_TARGET, "Secret key prepared successfully"); 46 | Ok(secret.private_key) 47 | } 48 | 49 | #[instrument(skip(self))] 50 | fn get_public_key(&self) -> Result { 51 | debug!(target: KEYSTORE_SIGNER_TARGET, "Retrieving first public key"); 52 | self.potential_pubkeys 53 | .first() 54 | .cloned() 55 | .ok_or(SignerError::PublicKeyIsNotAvailable) 56 | } 57 | } 58 | 59 | impl KeystoreSigner { 60 | pub fn new_with_pubkey(pub_key: PublicKey) -> Self { 61 | debug!(target: KEYSTORE_SIGNER_TARGET, "Creating new KeystoreSigner with public key"); 62 | Self { 63 | potential_pubkeys: vec![pub_key], 64 | } 65 | } 66 | 67 | #[instrument(skip(network), fields(account_id = %account_id, network_name = %network.network_name))] 68 | pub async fn search_for_keys( 69 | account_id: AccountId, 70 | network: &NetworkConfig, 71 | ) -> Result { 72 | info!(target: KEYSTORE_SIGNER_TARGET, "Searching for keys for account"); 73 | let account_keys = crate::account::Account(account_id.clone()) 74 | .list_keys() 75 | .fetch_from(network) 76 | .await?; 77 | 78 | debug!(target: KEYSTORE_SIGNER_TARGET, "Filtering and collecting potential public keys"); 79 | let potential_pubkeys = account_keys 80 | .keys 81 | .iter() 82 | // TODO: support functional access keys 83 | .filter(|key| key.access_key.permission == AccessKeyPermissionView::FullAccess) 84 | .map(|key| Self::get_secret_key(&account_id, &key.public_key, &network.network_name)); 85 | let potential_pubkeys: Vec = join_all(potential_pubkeys) 86 | .await 87 | .into_iter() 88 | .flat_map(|result| result.map(|keypair| keypair.public_key).ok()) 89 | .collect(); 90 | 91 | info!(target: KEYSTORE_SIGNER_TARGET, "KeystoreSigner created with {} potential public keys", potential_pubkeys.len()); 92 | Ok(Self { potential_pubkeys }) 93 | } 94 | 95 | #[instrument(skip(public_key), fields(account_id = %account_id, network_name = %network_name))] 96 | async fn get_secret_key( 97 | account_id: &AccountId, 98 | public_key: &PublicKey, 99 | network_name: &str, 100 | ) -> Result { 101 | trace!(target: KEYSTORE_SIGNER_TARGET, "Retrieving secret key from keyring"); 102 | let service_name = 103 | std::borrow::Cow::Owned(format!("near-{}-{}", network_name, account_id.as_str())); 104 | let user = format!("{}:{}", account_id, public_key); 105 | 106 | // This can be a blocking operation (for example, if the keyring is locked in the OS and user needs to unlock it), 107 | // so we need to spawn a new task to get the password 108 | let password = tokio::task::spawn_blocking(move || { 109 | let password = keyring::Entry::new(&service_name, &user)?.get_password()?; 110 | 111 | Ok::<_, KeyStoreError>(password) 112 | }) 113 | .await 114 | .unwrap_or_else(|tokio_join_error| Err(KeyStoreError::from(tokio_join_error)))?; 115 | 116 | debug!(target: KEYSTORE_SIGNER_TARGET, "Deserializing account key pair"); 117 | Ok(serde_json::from_str(&password)?) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/signer/ledger.rs: -------------------------------------------------------------------------------- 1 | use near_crypto::{PublicKey, SecretKey}; 2 | use near_primitives::{ 3 | action::delegate::SignedDelegateAction, transaction::Transaction, types::Nonce, 4 | }; 5 | use slipped10::BIP32Path; 6 | use tracing::{debug, info, instrument, trace, warn}; 7 | 8 | use crate::{ 9 | errors::{LedgerError, MetaSignError, SignerError}, 10 | types::{transactions::PrepopulateTransaction, CryptoHash}, 11 | }; 12 | 13 | use super::{NEP413Payload, SignerTrait}; 14 | 15 | const LEDGER_SIGNER_TARGET: &str = "near_api::signer::ledger"; 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct LedgerSigner { 19 | hd_path: BIP32Path, 20 | } 21 | 22 | impl LedgerSigner { 23 | pub const fn new(hd_path: BIP32Path) -> Self { 24 | Self { hd_path } 25 | } 26 | } 27 | 28 | #[async_trait::async_trait] 29 | impl SignerTrait for LedgerSigner { 30 | #[instrument(skip(self, tr), fields(signer_id = %tr.signer_id, receiver_id = %tr.receiver_id))] 31 | async fn sign( 32 | &self, 33 | tr: PrepopulateTransaction, 34 | public_key: PublicKey, 35 | nonce: Nonce, 36 | block_hash: CryptoHash, 37 | ) -> Result { 38 | debug!(target: LEDGER_SIGNER_TARGET, "Preparing unsigned transaction"); 39 | let mut unsigned_tx = Transaction::new_v0( 40 | tr.signer_id.clone(), 41 | public_key, 42 | tr.receiver_id, 43 | nonce, 44 | block_hash.into(), 45 | ); 46 | *unsigned_tx.actions_mut() = tr.actions; 47 | let unsigned_tx_bytes = borsh::to_vec(&unsigned_tx).map_err(LedgerError::from)?; 48 | let hd_path = self.hd_path.clone(); 49 | 50 | info!(target: LEDGER_SIGNER_TARGET, "Signing transaction with Ledger"); 51 | let signature = tokio::task::spawn_blocking(move || { 52 | let unsigned_tx_bytes = unsigned_tx_bytes; 53 | let signature = near_ledger::sign_transaction(&unsigned_tx_bytes, hd_path) 54 | .map_err(LedgerError::from)?; 55 | 56 | Ok::<_, LedgerError>(signature) 57 | }) 58 | .await 59 | .map_err(LedgerError::from)?; 60 | 61 | let signature = signature?; 62 | 63 | debug!(target: LEDGER_SIGNER_TARGET, "Creating Signature object"); 64 | let signature = 65 | near_crypto::Signature::from_parts(near_crypto::KeyType::ED25519, &signature) 66 | .map_err(LedgerError::from)?; 67 | 68 | info!(target: LEDGER_SIGNER_TARGET, "Transaction signed successfully"); 69 | Ok(near_primitives::transaction::SignedTransaction::new( 70 | signature, 71 | unsigned_tx, 72 | )) 73 | } 74 | 75 | #[instrument(skip(self, tr), fields(signer_id = %tr.signer_id, receiver_id = %tr.receiver_id))] 76 | async fn sign_meta( 77 | &self, 78 | tr: PrepopulateTransaction, 79 | public_key: PublicKey, 80 | nonce: Nonce, 81 | _block_hash: CryptoHash, 82 | max_block_height: near_primitives::types::BlockHeight, 83 | ) -> Result { 84 | debug!(target: LEDGER_SIGNER_TARGET, "Preparing delegate action"); 85 | let actions = tr 86 | .actions 87 | .into_iter() 88 | .map(near_primitives::action::delegate::NonDelegateAction::try_from) 89 | .collect::>() 90 | .map_err(|_| MetaSignError::DelegateActionIsNotSupported)?; 91 | let delegate_action = near_primitives::action::delegate::DelegateAction { 92 | sender_id: tr.signer_id, 93 | receiver_id: tr.receiver_id, 94 | actions, 95 | nonce, 96 | max_block_height, 97 | public_key, 98 | }; 99 | 100 | let delegate_action_bytes = borsh::to_vec(&delegate_action) 101 | .map_err(LedgerError::from) 102 | .map_err(SignerError::from)?; 103 | let hd_path = self.hd_path.clone(); 104 | 105 | info!(target: LEDGER_SIGNER_TARGET, "Signing delegate action with Ledger"); 106 | let signature = tokio::task::spawn_blocking(move || { 107 | let delegate_action_bytes = delegate_action_bytes; 108 | let signature = 109 | near_ledger::sign_message_nep366_delegate_action(&delegate_action_bytes, hd_path) 110 | .map_err(LedgerError::from)?; 111 | 112 | Ok::<_, LedgerError>(signature) 113 | }) 114 | .await 115 | .map_err(LedgerError::from) 116 | .map_err(SignerError::from)?; 117 | 118 | let signature = signature.map_err(SignerError::from)?; 119 | 120 | debug!(target: LEDGER_SIGNER_TARGET, "Creating Signature object for delegate action"); 121 | let signature = 122 | near_crypto::Signature::from_parts(near_crypto::KeyType::ED25519, &signature) 123 | .map_err(LedgerError::from) 124 | .map_err(SignerError::from)?; 125 | 126 | info!(target: LEDGER_SIGNER_TARGET, "Delegate action signed successfully"); 127 | Ok(SignedDelegateAction { 128 | delegate_action, 129 | signature, 130 | }) 131 | } 132 | 133 | #[instrument(skip(self), fields(signer_id = %_signer_id, receiver_id = %payload.recipient, message = %payload.message))] 134 | async fn sign_message_nep413( 135 | &self, 136 | _signer_id: crate::AccountId, 137 | _public_key: PublicKey, 138 | payload: NEP413Payload, 139 | ) -> Result { 140 | info!(target: LEDGER_SIGNER_TARGET, "Signing NEP413 message with Ledger"); 141 | let hd_path = self.hd_path.clone(); 142 | let payload = payload.into(); 143 | 144 | let signature: Vec = tokio::task::spawn_blocking(move || { 145 | let signature = 146 | near_ledger::sign_message_nep413(&payload, hd_path).map_err(LedgerError::from)?; 147 | 148 | Ok::<_, LedgerError>(signature) 149 | }) 150 | .await 151 | .unwrap_or_else(|tokio_join_error| Err(LedgerError::from(tokio_join_error)))?; 152 | 153 | debug!(target: LEDGER_SIGNER_TARGET, "Creating Signature object for NEP413"); 154 | let signature = 155 | near_crypto::Signature::from_parts(near_crypto::KeyType::ED25519, &signature) 156 | .map_err(LedgerError::from)?; 157 | 158 | Ok(signature) 159 | } 160 | 161 | async fn get_secret_key( 162 | &self, 163 | _signer_id: &crate::AccountId, 164 | _public_key: &PublicKey, 165 | ) -> Result { 166 | warn!(target: LEDGER_SIGNER_TARGET, "Attempted to access secret key, which is not available for Ledger signer"); 167 | Err(SignerError::SecretKeyIsNotAvailable) 168 | } 169 | 170 | #[instrument(skip(self))] 171 | fn get_public_key(&self) -> Result { 172 | let public_key = near_ledger::get_wallet_id(self.hd_path.clone()) 173 | .map_err(|_| SignerError::PublicKeyIsNotAvailable)?; 174 | 175 | trace!(target: LEDGER_SIGNER_TARGET, "Public key retrieved successfully"); 176 | Ok(near_crypto::PublicKey::ED25519( 177 | near_crypto::ED25519PublicKey::from(public_key.to_bytes()), 178 | )) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/signer/secret_key.rs: -------------------------------------------------------------------------------- 1 | use near_crypto::{PublicKey, SecretKey}; 2 | use tracing::{instrument, trace}; 3 | 4 | use crate::errors::SignerError; 5 | 6 | use super::SignerTrait; 7 | 8 | const SECRET_KEY_SIGNER_TARGET: &str = "near_api::signer::secret_key"; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct SecretKeySigner { 12 | secret_key: SecretKey, 13 | public_key: PublicKey, 14 | } 15 | 16 | #[async_trait::async_trait] 17 | impl SignerTrait for SecretKeySigner { 18 | #[instrument(skip(self))] 19 | async fn get_secret_key( 20 | &self, 21 | signer_id: &crate::AccountId, 22 | public_key: &PublicKey, 23 | ) -> Result { 24 | trace!(target: SECRET_KEY_SIGNER_TARGET, "returning with secret key"); 25 | Ok(self.secret_key.clone()) 26 | } 27 | 28 | #[instrument(skip(self))] 29 | fn get_public_key(&self) -> Result { 30 | Ok(self.public_key.clone()) 31 | } 32 | } 33 | 34 | impl SecretKeySigner { 35 | pub fn new(secret_key: SecretKey) -> Self { 36 | let public_key = secret_key.public_key(); 37 | Self { 38 | secret_key, 39 | public_key, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | use near_token::NearToken; 2 | use serde_json::json; 3 | 4 | use crate::{ 5 | common::query::{CallResultHandler, PostprocessHandler, QueryBuilder}, 6 | contract::{Contract, ContractTransactBuilder}, 7 | errors::BuilderError, 8 | transactions::ConstructTransaction, 9 | types::storage::{StorageBalance, StorageBalanceInternal}, 10 | AccountId, Data, 11 | }; 12 | 13 | ///A wrapper struct that simplifies interactions with the [Storage Management](https://github.com/near/NEPs/blob/master/neps/nep-0145.md) standard 14 | /// 15 | /// Contracts on NEAR Protocol often implement a [NEP-145](https://github.com/near/NEPs/blob/master/neps/nep-0145.md) for managing storage deposits, 16 | /// which are required for storing data on the blockchain. This struct provides convenient methods 17 | /// to interact with these storage-related functions on the contract. 18 | /// 19 | /// # Example 20 | /// ``` 21 | /// use near_api::*; 22 | /// 23 | /// # async fn example() -> Result<(), Box> { 24 | /// let storage = StorageDeposit::on_contract("contract.testnet".parse()?); 25 | /// 26 | /// // Check storage balance 27 | /// let balance = storage.view_account_storage("alice.testnet".parse()?)?.fetch_from_testnet().await?; 28 | /// println!("Storage balance: {:?}", balance); 29 | /// 30 | /// // Bob pays for Alice's storage on the contract contract.testnet 31 | /// let deposit_tx = storage.deposit("alice.testnet".parse()?, NearToken::from_near(1))? 32 | /// .with_signer("bob.testnet".parse()?, Signer::new(Signer::from_ledger())?) 33 | /// .send_to_testnet() 34 | /// .await 35 | /// .unwrap(); 36 | /// # Ok(()) 37 | /// # } 38 | /// ``` 39 | #[derive(Clone, Debug)] 40 | pub struct StorageDeposit(AccountId); 41 | 42 | impl StorageDeposit { 43 | pub const fn on_contract(contract_id: AccountId) -> Self { 44 | Self(contract_id) 45 | } 46 | 47 | /// Prepares a new contract query (`storage_balance_of`) for fetching the storage balance (Option<[StorageBalance]>) of the account on the contract. 48 | /// 49 | /// ## Example 50 | /// ```rust,no_run 51 | /// use near_api::*; 52 | /// 53 | /// # async fn example() -> Result<(), Box> { 54 | /// let balance = StorageDeposit::on_contract("contract.testnet".parse()?) 55 | /// .view_account_storage("alice.testnet".parse()?)? 56 | /// .fetch_from_testnet() 57 | /// .await?; 58 | /// println!("Storage balance: {:?}", balance); 59 | /// # Ok(()) 60 | /// # } 61 | /// ``` 62 | #[allow(clippy::complexity)] 63 | pub fn view_account_storage( 64 | &self, 65 | account_id: AccountId, 66 | ) -> Result< 67 | QueryBuilder< 68 | PostprocessHandler< 69 | Data>, 70 | CallResultHandler>, 71 | >, 72 | >, 73 | BuilderError, 74 | > { 75 | Ok(Contract(self.0.clone()) 76 | .call_function( 77 | "storage_balance_of", 78 | json!({ 79 | "account_id": account_id, 80 | }), 81 | )? 82 | .read_only() 83 | .map(|storage: Data>| { 84 | storage.map(|option_storage| { 85 | option_storage.map(|data| StorageBalance { 86 | available: data.available, 87 | total: data.total, 88 | locked: NearToken::from_yoctonear( 89 | data.total.as_yoctonear() - data.available.as_yoctonear(), 90 | ), 91 | }) 92 | }) 93 | })) 94 | } 95 | 96 | /// Prepares a new transaction contract call (`storage_deposit`) for depositing storage on the contract. 97 | /// 98 | /// ## Example 99 | /// ```rust,no_run 100 | /// use near_api::*; 101 | /// 102 | /// # async fn example() -> Result<(), Box> { 103 | /// let tx: near_primitives::views::FinalExecutionOutcomeView = StorageDeposit::on_contract("contract.testnet".parse()?) 104 | /// .deposit("alice.testnet".parse()?, NearToken::from_near(1))? 105 | /// .with_signer("bob.testnet".parse()?, Signer::new(Signer::from_ledger())?) 106 | /// .send_to_testnet() 107 | /// .await?; 108 | /// # Ok(()) 109 | /// # } 110 | /// ``` 111 | pub fn deposit( 112 | &self, 113 | receiver_account_id: AccountId, 114 | amount: NearToken, 115 | ) -> Result { 116 | Ok(Contract(self.0.clone()) 117 | .call_function( 118 | "storage_deposit", 119 | json!({ 120 | "account_id": receiver_account_id.to_string(), 121 | }), 122 | )? 123 | .transaction() 124 | .deposit(amount)) 125 | } 126 | 127 | /// Prepares a new transaction contract call (`storage_withdraw`) for withdrawing storage from the contract. 128 | /// 129 | /// ## Example 130 | /// ```rust,no_run 131 | /// use near_api::*; 132 | /// 133 | /// # async fn example() -> Result<(), Box> { 134 | /// let tx: near_primitives::views::FinalExecutionOutcomeView = StorageDeposit::on_contract("contract.testnet".parse()?) 135 | /// .withdraw("alice.testnet".parse()?, NearToken::from_near(1))? 136 | /// .with_signer(Signer::new(Signer::from_ledger())?) 137 | /// .send_to_testnet() 138 | /// .await?; 139 | /// # Ok(()) 140 | /// # } 141 | /// ``` 142 | pub fn withdraw( 143 | &self, 144 | account_id: AccountId, 145 | amount: NearToken, 146 | ) -> Result { 147 | Ok(Contract(self.0.clone()) 148 | .call_function( 149 | "storage_withdraw", 150 | json!({ 151 | "amount": amount.as_yoctonear() 152 | }), 153 | )? 154 | .transaction() 155 | .deposit(NearToken::from_yoctonear(1)) 156 | .with_signer_account(account_id)) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/tokens.rs: -------------------------------------------------------------------------------- 1 | use near_contract_standards::{ 2 | fungible_token::metadata::FungibleTokenMetadata, 3 | non_fungible_token::{metadata::NFTContractMetadata, Token}, 4 | }; 5 | use near_primitives::{ 6 | action::{Action, TransferAction}, 7 | types::{AccountId, BlockReference}, 8 | }; 9 | use near_sdk::json_types::U128; 10 | use near_token::NearToken; 11 | use serde_json::json; 12 | 13 | use crate::{ 14 | common::{ 15 | query::{ 16 | AccountViewHandler, CallResultHandler, MultiQueryBuilder, MultiQueryHandler, 17 | PostprocessHandler, QueryBuilder, SimpleQuery, 18 | }, 19 | send::Transactionable, 20 | }, 21 | contract::Contract, 22 | errors::{BuilderError, FTValidatorError, ValidationError}, 23 | transactions::{ConstructTransaction, TransactionWithSign}, 24 | types::{ 25 | tokens::{FTBalance, UserBalance, STORAGE_COST_PER_BYTE}, 26 | transactions::PrepopulateTransaction, 27 | }, 28 | Data, NetworkConfig, StorageDeposit, 29 | }; 30 | 31 | type Result = core::result::Result; 32 | 33 | // This is not too long as most of the size is a links to the docs 34 | #[allow(clippy::too_long_first_doc_paragraph)] 35 | /// A wrapper struct that simplifies interactions with 36 | /// [NEAR](https://docs.near.org/concepts/basics/tokens), 37 | /// [FT](https://docs.near.org/build/primitives/ft), 38 | /// [NFT](https://docs.near.org/build/primitives/nft) 39 | /// 40 | /// This struct provides convenient methods to interact with different types of tokens on NEAR Protocol: 41 | /// - [Native NEAR](https://docs.near.org/concepts/basics/tokens) token operations 42 | /// - Fungible Token - [Documentation and examples](https://docs.near.org/build/primitives/ft), [NEP-141](https://github.com/near/NEPs/blob/master/neps/nep-0141.md) 43 | /// - Non-Fungible Token - [Documentation and examples](https://docs.near.org/build/primitives/nft), [NEP-171](https://github.com/near/NEPs/blob/master/neps/nep-0171.md) 44 | /// 45 | /// ## Examples 46 | /// 47 | /// ### Fungible Token Operations 48 | /// ``` 49 | /// use near_api::*; 50 | /// 51 | /// # async fn example() -> Result<(), Box> { 52 | /// let bob_tokens = Tokens::account("bob.testnet".parse()?); 53 | /// 54 | /// // Check FT balance 55 | /// let balance = bob_tokens.ft_balance("usdt.tether-token.near".parse()?)?.fetch_from_mainnet().await?; 56 | /// println!("Bob balance: {}", balance); 57 | /// 58 | /// // Transfer FT tokens 59 | /// bob_tokens.send_to("alice.testnet".parse()?) 60 | /// .ft( 61 | /// "usdt.tether-token.near".parse()?, 62 | /// USDT_BALANCE.with_whole_amount(100) 63 | /// )? 64 | /// .with_signer(Signer::new(Signer::from_ledger())?) 65 | /// .send_to_mainnet() 66 | /// .await?; 67 | /// # Ok(()) 68 | /// # } 69 | /// ``` 70 | /// 71 | /// ### NFT Operations 72 | /// ``` 73 | /// use near_api::*; 74 | /// 75 | /// # async fn example() -> Result<(), Box> { 76 | /// let alice_tokens = Tokens::account("alice.testnet".parse()?); 77 | /// 78 | /// // Check NFT assets 79 | /// let tokens = alice_tokens.nft_assets("nft-contract.testnet".parse()?)?.fetch_from_testnet().await?; 80 | /// println!("NFT count: {}", tokens.data.len()); 81 | /// 82 | /// // Transfer NFT 83 | /// alice_tokens.send_to("bob.testnet".parse()?) 84 | /// .nft("nft-contract.testnet".parse()?, "token-id".to_string())? 85 | /// .with_signer(Signer::new(Signer::from_ledger())?) 86 | /// .send_to_testnet() 87 | /// .await?; 88 | /// # Ok(()) 89 | /// # } 90 | /// ``` 91 | /// 92 | /// ### NEAR Token Operations 93 | /// ``` 94 | /// use near_api::*; 95 | /// 96 | /// # async fn example() -> Result<(), Box> { 97 | /// let alice_account = Tokens::account("alice.testnet".parse()?); 98 | /// 99 | /// // Check NEAR balance 100 | /// let balance = alice_account.near_balance().fetch_from_testnet().await?; 101 | /// println!("NEAR balance: {}", balance.total); 102 | /// 103 | /// // Send NEAR 104 | /// alice_account.send_to("bob.testnet".parse()?) 105 | /// .near(NearToken::from_near(1)) 106 | /// .with_signer(Signer::new(Signer::from_ledger())?) 107 | /// .send_to_testnet() 108 | /// .await?; 109 | /// # Ok(()) 110 | /// # } 111 | /// ``` 112 | #[derive(Debug, Clone)] 113 | pub struct Tokens { 114 | account_id: AccountId, 115 | } 116 | 117 | impl Tokens { 118 | pub const fn account(account_id: AccountId) -> Self { 119 | Self { account_id } 120 | } 121 | 122 | /// Fetches the total NEAR balance ([UserBalance]) of the account. 123 | /// 124 | /// ## Example 125 | /// ```rust,no_run 126 | /// use near_api::*; 127 | /// 128 | /// # async fn example() -> Result<(), Box> { 129 | /// let alice_tokens = Tokens::account("alice.testnet".parse()?); 130 | /// let balance = alice_tokens.near_balance().fetch_from_testnet().await?; 131 | /// println!("Alice's NEAR balance: {:?}", balance); 132 | /// # Ok(()) 133 | /// # } 134 | /// ``` 135 | pub fn near_balance( 136 | &self, 137 | ) -> QueryBuilder> { 138 | let request = near_primitives::views::QueryRequest::ViewAccount { 139 | account_id: self.account_id.clone(), 140 | }; 141 | 142 | QueryBuilder::new( 143 | SimpleQuery { request }, 144 | BlockReference::latest(), 145 | AccountViewHandler, 146 | ) 147 | .map(|account| { 148 | let account = account.data; 149 | let total = NearToken::from_yoctonear(account.amount); 150 | let storage_locked = NearToken::from_yoctonear( 151 | account.storage_usage as u128 * STORAGE_COST_PER_BYTE.as_yoctonear(), 152 | ); 153 | let locked = NearToken::from_yoctonear(account.locked); 154 | let storage_usage = account.storage_usage; 155 | UserBalance { 156 | total, 157 | storage_locked, 158 | storage_usage, 159 | locked, 160 | } 161 | }) 162 | } 163 | 164 | /// Prepares a new contract query (`nft_metadata`) for fetching the NFT metadata ([NFTContractMetadata]). 165 | /// 166 | /// The function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171) 167 | /// 168 | /// ## Example 169 | /// ```rust,no_run 170 | /// use near_api::*; 171 | /// 172 | /// # async fn example() -> Result<(), Box> { 173 | /// let metadata = Tokens::nft_metadata("nft-contract.testnet".parse()?)? 174 | /// .fetch_from_testnet() 175 | /// .await?; 176 | /// println!("NFT metadata: {:?}", metadata); 177 | /// # Ok(()) 178 | /// # } 179 | /// ``` 180 | pub fn nft_metadata( 181 | contract_id: AccountId, 182 | ) -> Result>> { 183 | Ok(Contract(contract_id) 184 | .call_function("nft_metadata", ())? 185 | .read_only()) 186 | } 187 | 188 | /// Prepares a new contract query (`nft_tokens_for_owner`) for fetching the NFT assets of the account ([Vec]<[Token]>). 189 | /// 190 | /// The function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171) 191 | /// 192 | /// ## Example 193 | /// ```rust,no_run 194 | /// use near_api::*; 195 | /// 196 | /// # async fn example() -> Result<(), Box> { 197 | /// let alice_tokens = Tokens::account("alice.testnet".parse()?); 198 | /// let alice_assets = alice_tokens.nft_assets("nft-contract.testnet".parse()?)? 199 | /// .fetch_from_testnet() 200 | /// .await?; 201 | /// println!("Alice's NFT assets: {:?}", alice_assets); 202 | /// # Ok(()) 203 | /// # } 204 | /// ``` 205 | pub fn nft_assets( 206 | &self, 207 | nft_contract: AccountId, 208 | ) -> Result>>> { 209 | Ok(Contract(nft_contract) 210 | .call_function( 211 | "nft_tokens_for_owner", 212 | json!({ 213 | "account_id": self.account_id.to_string(), 214 | }), 215 | )? 216 | .read_only()) 217 | } 218 | 219 | /// Prepares a new contract query (`ft_metadata`) for fetching the FT metadata ([FungibleTokenMetadata]). 220 | /// 221 | /// The function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141) 222 | /// 223 | /// ## Example 224 | /// ```rust,no_run 225 | /// use near_api::*; 226 | /// 227 | /// # async fn example() -> Result<(), Box> { 228 | /// let metadata = Tokens::ft_metadata("usdt.tether-token.near".parse()?)? 229 | /// .fetch_from_testnet() 230 | /// .await? 231 | /// .data; 232 | /// println!("FT metadata: {} {}", metadata.name, metadata.symbol); 233 | /// # Ok(()) 234 | /// # } 235 | /// ``` 236 | pub fn ft_metadata( 237 | contract_id: AccountId, 238 | ) -> Result>> { 239 | Ok(Contract(contract_id) 240 | .call_function("ft_metadata", ())? 241 | .read_only()) 242 | } 243 | 244 | /// Prepares a new contract query (`ft_balance_of`, `ft_metadata`) for fetching the [FTBalance] of the account. 245 | /// 246 | /// This query is a multi-query, meaning it will fetch the FT metadata and the FT balance of the account. 247 | /// The result is then postprocessed to create a `FTBalance` instance. 248 | /// 249 | /// The function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141) 250 | /// 251 | /// # Example 252 | /// ```rust,no_run 253 | /// use near_api::*; 254 | /// 255 | /// # async fn example() -> Result<(), Box> { 256 | /// let alice_usdt_balance = Tokens::account("alice.near".parse()?) 257 | /// .ft_balance("usdt.tether-token.near".parse()?)? 258 | /// .fetch_from_mainnet() 259 | /// .await?; 260 | /// println!("Alice's USDT balance: {}", alice_usdt_balance); 261 | /// # Ok(()) 262 | /// # } 263 | /// ``` 264 | #[allow(clippy::complexity)] 265 | pub fn ft_balance( 266 | &self, 267 | ft_contract: AccountId, 268 | ) -> Result< 269 | MultiQueryBuilder< 270 | PostprocessHandler< 271 | FTBalance, 272 | MultiQueryHandler<( 273 | CallResultHandler, 274 | CallResultHandler, 275 | )>, 276 | >, 277 | >, 278 | > { 279 | let handler = MultiQueryHandler::new(( 280 | CallResultHandler::::new(), 281 | CallResultHandler::default(), 282 | )); 283 | let multiquery = MultiQueryBuilder::new(handler, BlockReference::latest()) 284 | .add_query_builder(Self::ft_metadata(ft_contract.clone())?) 285 | .add_query_builder( 286 | Contract(ft_contract) 287 | .call_function( 288 | "ft_balance_of", 289 | json!({ 290 | "account_id": self.account_id.clone() 291 | }), 292 | )? 293 | .read_only::<()>(), 294 | ) 295 | .map( 296 | |(metadata, amount): (Data, Data)| { 297 | FTBalance::with_decimals(metadata.data.decimals).with_amount(amount.data.0) 298 | }, 299 | ); 300 | Ok(multiquery) 301 | } 302 | 303 | /// Prepares a new transaction builder for sending tokens to another account. 304 | /// 305 | /// This builder is used to construct transactions for sending NEAR, FT, and NFT tokens. 306 | /// 307 | /// ## Sending NEAR 308 | /// ```rust,no_run 309 | /// use near_api::*; 310 | /// 311 | /// # async fn example() -> Result<(), Box> { 312 | /// let alice_tokens = Tokens::account("alice.near".parse()?); 313 | /// 314 | /// let result: near_primitives::views::FinalExecutionOutcomeView = alice_tokens.send_to("bob.near".parse()?) 315 | /// .near(NearToken::from_near(1)) 316 | /// .with_signer(Signer::new(Signer::from_ledger())?) 317 | /// .send_to_mainnet() 318 | /// .await?; 319 | /// # Ok(()) 320 | /// # } 321 | /// ``` 322 | /// 323 | /// ## Sending FT 324 | /// ```rust,no_run 325 | /// use near_api::*; 326 | /// 327 | /// # async fn example() -> Result<(), Box> { 328 | /// let alice_tokens = Tokens::account("alice.near".parse()?); 329 | /// 330 | /// let result: near_primitives::views::FinalExecutionOutcomeView = alice_tokens.send_to("bob.near".parse()?) 331 | /// .ft("usdt.tether-token.near".parse()?, USDT_BALANCE.with_whole_amount(100))? 332 | /// .with_signer(Signer::new(Signer::from_ledger())?) 333 | /// .send_to_mainnet() 334 | /// .await?; 335 | /// # Ok(()) 336 | /// # } 337 | /// ``` 338 | /// 339 | /// ## Sending NFT 340 | /// ```rust,no_run 341 | /// use near_api::*; 342 | /// 343 | /// # async fn example() -> Result<(), Box> { 344 | /// let alice_tokens = Tokens::account("alice.near".parse()?); 345 | /// 346 | /// let result: near_primitives::views::FinalExecutionOutcomeView = alice_tokens.send_to("bob.near".parse()?) 347 | /// .nft("nft-contract.testnet".parse()?, "token-id".to_string())? 348 | /// .with_signer(Signer::new(Signer::from_ledger())?) 349 | /// .send_to_testnet() 350 | /// .await?; 351 | /// # Ok(()) 352 | /// # } 353 | /// ``` 354 | pub fn send_to(&self, receiver_id: AccountId) -> SendToBuilder { 355 | SendToBuilder { 356 | from: self.account_id.clone(), 357 | receiver_id, 358 | } 359 | } 360 | } 361 | 362 | #[derive(Debug, Clone)] 363 | pub struct SendToBuilder { 364 | from: AccountId, 365 | receiver_id: AccountId, 366 | } 367 | 368 | impl SendToBuilder { 369 | /// Prepares a new transaction for sending NEAR tokens to another account. 370 | pub fn near(self, amount: NearToken) -> ConstructTransaction { 371 | ConstructTransaction::new(self.from, self.receiver_id).add_action(Action::Transfer( 372 | TransferAction { 373 | deposit: amount.as_yoctonear(), 374 | }, 375 | )) 376 | } 377 | 378 | /// Prepares a new transaction contract call (`ft_transfer`, `ft_metadata`, `storage_balance_of`, `storage_deposit`) for sending FT tokens to another account. 379 | /// 380 | /// Please note that if the receiver does not have enough storage, we will automatically deposit 100 milliNEAR for storage from 381 | /// the sender. 382 | /// 383 | /// The provided function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141) 384 | pub fn ft( 385 | self, 386 | ft_contract: AccountId, 387 | amount: FTBalance, 388 | ) -> Result> { 389 | let tr = Contract(ft_contract) 390 | .call_function( 391 | "ft_transfer", 392 | json!({ 393 | "receiver_id": self.receiver_id, 394 | "amount": U128(amount.amount()), 395 | }), 396 | )? 397 | .transaction() 398 | .deposit(NearToken::from_yoctonear(1)) 399 | .with_signer_account(self.from); 400 | 401 | Ok(TransactionWithSign { 402 | tx: FTTransactionable { 403 | receiver: self.receiver_id, 404 | prepopulated: tr.tr, 405 | decimals: amount.decimals(), 406 | }, 407 | }) 408 | } 409 | 410 | /// Prepares a new transaction contract call (`nft_transfer`) for sending NFT tokens to another account. 411 | /// 412 | /// The provided function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171) 413 | pub fn nft(self, nft_contract: AccountId, token_id: String) -> Result { 414 | Ok(Contract(nft_contract) 415 | .call_function( 416 | "nft_transfer", 417 | json!({ 418 | "receiver_id": self.receiver_id, 419 | "token_id": token_id 420 | }), 421 | )? 422 | .transaction() 423 | .deposit(NearToken::from_yoctonear(1)) 424 | .with_signer_account(self.from)) 425 | } 426 | } 427 | 428 | /// The structs validates the decimals correctness on runtime level before 429 | /// sending the ft tokens as well as deposits 100 milliNear of the deposit if 430 | /// the receiver doesn't have any allocated storage in the provided FT contract 431 | #[derive(Clone, Debug)] 432 | pub struct FTTransactionable { 433 | prepopulated: PrepopulateTransaction, 434 | receiver: AccountId, 435 | decimals: u8, 436 | } 437 | 438 | impl FTTransactionable { 439 | pub async fn check_decimals( 440 | &self, 441 | network: &NetworkConfig, 442 | ) -> core::result::Result<(), ValidationError> { 443 | let metadata = Tokens::ft_metadata(self.prepopulated.receiver_id.clone())?; 444 | 445 | let metadata = metadata 446 | .fetch_from(network) 447 | .await 448 | .map_err(|_| FTValidatorError::NoMetadata)?; 449 | if metadata.data.decimals != self.decimals { 450 | Err(FTValidatorError::DecimalsMismatch { 451 | expected: metadata.data.decimals, 452 | got: self.decimals, 453 | })?; 454 | } 455 | Ok(()) 456 | } 457 | } 458 | 459 | #[async_trait::async_trait] 460 | impl Transactionable for FTTransactionable { 461 | fn prepopulated(&self) -> PrepopulateTransaction { 462 | self.prepopulated.clone() 463 | } 464 | 465 | async fn validate_with_network( 466 | &self, 467 | network: &NetworkConfig, 468 | ) -> core::result::Result<(), ValidationError> { 469 | self.check_decimals(network).await?; 470 | 471 | let storage_balance = StorageDeposit::on_contract(self.prepopulated.receiver_id.clone()) 472 | .view_account_storage(self.receiver.clone())? 473 | .fetch_from(network) 474 | .await?; 475 | 476 | if storage_balance.data.is_none() { 477 | Err(FTValidatorError::StorageDepositNeeded)?; 478 | } 479 | 480 | Ok(()) 481 | } 482 | 483 | async fn edit_with_network( 484 | &mut self, 485 | network: &NetworkConfig, 486 | ) -> core::result::Result<(), ValidationError> { 487 | self.check_decimals(network).await?; 488 | 489 | let storage_balance = StorageDeposit::on_contract(self.prepopulated.receiver_id.clone()) 490 | .view_account_storage(self.receiver.clone())? 491 | .fetch_from(network) 492 | .await?; 493 | 494 | if storage_balance.data.is_none() { 495 | let mut action = StorageDeposit::on_contract(self.prepopulated.receiver_id.clone()) 496 | .deposit(self.receiver.clone(), NearToken::from_millinear(100))? 497 | .with_signer_account(self.prepopulated.signer_id.clone()) 498 | .tr 499 | .actions; 500 | action.append(&mut self.prepopulated.actions); 501 | self.prepopulated.actions = action; 502 | } 503 | Ok(()) 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /src/transactions.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use near_primitives::{action::Action, types::AccountId}; 4 | 5 | use crate::{ 6 | common::send::{ExecuteSignedTransaction, Transactionable}, 7 | config::NetworkConfig, 8 | errors::{SignerError, ValidationError}, 9 | signer::Signer, 10 | types::transactions::PrepopulateTransaction, 11 | }; 12 | 13 | #[derive(Clone, Debug)] 14 | pub struct TransactionWithSign { 15 | pub tx: T, 16 | } 17 | 18 | impl TransactionWithSign { 19 | pub fn with_signer(self, signer: Arc) -> ExecuteSignedTransaction { 20 | ExecuteSignedTransaction::new(self.tx, signer) 21 | } 22 | } 23 | 24 | #[derive(Clone, Debug)] 25 | pub struct SelfActionBuilder { 26 | pub actions: Vec, 27 | } 28 | 29 | impl Default for SelfActionBuilder { 30 | fn default() -> Self { 31 | Self::new() 32 | } 33 | } 34 | 35 | impl SelfActionBuilder { 36 | pub const fn new() -> Self { 37 | Self { 38 | actions: Vec::new(), 39 | } 40 | } 41 | 42 | /// Adds an action to the transaction. 43 | pub fn add_action(mut self, action: Action) -> Self { 44 | self.actions.push(action); 45 | self 46 | } 47 | 48 | /// Adds multiple actions to the transaction. 49 | pub fn add_actions(mut self, actions: Vec) -> Self { 50 | self.actions.extend(actions); 51 | self 52 | } 53 | 54 | /// Signs the transaction with the given account id and signer related to it. 55 | pub fn with_signer( 56 | self, 57 | signer_account_id: AccountId, 58 | signer: Arc, 59 | ) -> ExecuteSignedTransaction { 60 | ConstructTransaction::new(signer_account_id.clone(), signer_account_id) 61 | .add_actions(self.actions) 62 | .with_signer(signer) 63 | } 64 | } 65 | 66 | /// A builder for constructing transactions using Actions. 67 | #[derive(Debug, Clone)] 68 | pub struct ConstructTransaction { 69 | pub tr: PrepopulateTransaction, 70 | } 71 | 72 | impl ConstructTransaction { 73 | /// Pre-populates a transaction with the given signer and receiver IDs. 74 | pub const fn new(signer_id: AccountId, receiver_id: AccountId) -> Self { 75 | Self { 76 | tr: PrepopulateTransaction { 77 | signer_id, 78 | receiver_id, 79 | actions: Vec::new(), 80 | }, 81 | } 82 | } 83 | 84 | /// Adds an action to the transaction. 85 | pub fn add_action(mut self, action: Action) -> Self { 86 | self.tr.actions.push(action); 87 | self 88 | } 89 | 90 | /// Adds multiple actions to the transaction. 91 | pub fn add_actions(mut self, actions: Vec) -> Self { 92 | self.tr.actions.extend(actions); 93 | self 94 | } 95 | 96 | /// Signs the transaction with the given signer. 97 | pub fn with_signer(self, signer: Arc) -> ExecuteSignedTransaction { 98 | ExecuteSignedTransaction::new(self, signer) 99 | } 100 | } 101 | 102 | #[async_trait::async_trait] 103 | impl Transactionable for ConstructTransaction { 104 | fn prepopulated(&self) -> PrepopulateTransaction { 105 | PrepopulateTransaction { 106 | signer_id: self.tr.signer_id.clone(), 107 | receiver_id: self.tr.receiver_id.clone(), 108 | actions: self.tr.actions.clone(), 109 | } 110 | } 111 | 112 | async fn validate_with_network(&self, _: &NetworkConfig) -> Result<(), ValidationError> { 113 | Ok(()) 114 | } 115 | } 116 | 117 | /// Transaction related functionality. 118 | /// 119 | /// This struct provides ability to interact with transactions. 120 | #[derive(Clone, Debug)] 121 | pub struct Transaction; 122 | 123 | impl Transaction { 124 | /// Constructs a new transaction builder with the given signer and receiver IDs. 125 | /// This pattern is useful for batching actions into a single transaction. 126 | /// 127 | /// This is the low level interface for constructing transactions. 128 | /// It is designed to be used in scenarios where more control over the transaction process is required. 129 | /// 130 | /// # Example 131 | /// 132 | /// This example constructs a transaction with a two transfer actions. 133 | /// 134 | /// ```rust,no_run 135 | /// use near_api::*; 136 | /// use near_primitives::transaction::{Action, TransferAction}; 137 | /// 138 | /// # async fn example() -> Result<(), Box> { 139 | /// let signer = Signer::new(Signer::from_ledger())?; 140 | /// 141 | /// let transaction_result: near_primitives::views::FinalExecutionOutcomeView = Transaction::construct( 142 | /// "sender.near".parse()?, 143 | /// "receiver.near".parse()? 144 | /// ) 145 | /// .add_action(Action::Transfer( 146 | /// TransferAction { 147 | /// deposit: NearToken::from_near(1).as_yoctonear(), 148 | /// }, 149 | /// )) 150 | /// .add_action(Action::Transfer( 151 | /// TransferAction { 152 | /// deposit: NearToken::from_near(1).as_yoctonear(), 153 | /// }, 154 | /// )) 155 | /// .with_signer(signer) 156 | /// .send_to_mainnet() 157 | /// .await?; 158 | /// # Ok(()) 159 | /// # } 160 | /// ``` 161 | pub const fn construct(signer_id: AccountId, receiver_id: AccountId) -> ConstructTransaction { 162 | ConstructTransaction::new(signer_id, receiver_id) 163 | } 164 | 165 | /// Signs a transaction with the given signer. 166 | /// 167 | /// This provides ability to sign custom constructed pre-populated transactions. 168 | /// 169 | /// # Examples 170 | /// 171 | /// ```rust,no_run 172 | /// use near_api::*; 173 | /// use near_primitives::transaction::{Action, TransferAction}; 174 | /// 175 | /// # async fn example() -> Result<(), Box> { 176 | /// let signer = Signer::new(Signer::from_ledger())?; 177 | /// # let unsigned_tx = todo!(); 178 | /// 179 | /// let transaction_result: near_primitives::views::FinalExecutionOutcomeView = Transaction::sign_transaction( 180 | /// unsigned_tx, 181 | /// signer 182 | /// ) 183 | /// .await? 184 | /// .send_to_mainnet() 185 | /// .await?; 186 | /// # Ok(()) 187 | /// # } 188 | /// ``` 189 | pub async fn sign_transaction( 190 | unsigned_tx: near_primitives::transaction::Transaction, 191 | signer: Arc, 192 | ) -> Result { 193 | let public_key = unsigned_tx.public_key().clone(); 194 | let block_hash = *unsigned_tx.block_hash(); 195 | let nonce = unsigned_tx.nonce(); 196 | 197 | ConstructTransaction::new( 198 | unsigned_tx.signer_id().clone(), 199 | unsigned_tx.receiver_id().clone(), 200 | ) 201 | .add_actions(unsigned_tx.take_actions()) 202 | .with_signer(signer) 203 | .presign_offline(public_key, block_hash.into(), nonce) 204 | .await 205 | } 206 | 207 | // TODO: fetch transaction status 208 | // TODO: fetch transaction receipt 209 | // TODO: fetch transaction proof 210 | } 211 | -------------------------------------------------------------------------------- /src/types/contract.rs: -------------------------------------------------------------------------------- 1 | pub use build_info::BuildInfo; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// The struct provides information about deployed contract's source code and supported standards. 5 | /// 6 | /// Contract source metadata follows [**NEP-330 standard**](https://github.com/near/NEPs/blob/master/neps/nep-0330.md) for smart contracts 7 | /// 8 | /// See the documentation of [`Contract::contract_source_metadata`](crate::Contract::contract_source_metadata) on how to query this for a contract via this crate 9 | // `rustdoc` clearly lacks functionality of automatic backlinks within a single crate 10 | #[derive(Debug, Clone, PartialEq, Default, Eq, Serialize, Deserialize)] 11 | pub struct ContractSourceMetadata { 12 | /// Optional version identifier, typically a semantic version 13 | /// 14 | /// ## Examples: 15 | /// 16 | /// ```rust,no_run 17 | /// # let version: Option = 18 | /// // Semantic version 19 | /// Some("1.0.0".into()) 20 | /// # ; 21 | /// ``` 22 | /// ```rust,no_run 23 | /// # let version: Option = 24 | /// // Git commit 25 | /// Some("39f2d2646f2f60e18ab53337501370dc02a5661c".into()) 26 | /// # ; 27 | /// ``` 28 | pub version: Option, 29 | 30 | // cSpell::ignore bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq 31 | /// Optional URL to source code repository/tree 32 | /// 33 | /// ## Examples: 34 | /// 35 | /// ```rust,no_run 36 | /// # let link: Option = 37 | /// // GitHub URL 38 | /// Some("https://github.com/org/repo/tree/8d8a8a0fe86a1d8eb3bce45f04ab1a65fecf5a1b".into()) 39 | /// # ; 40 | /// ``` 41 | /// ```rust,no_run 42 | /// # let link: Option = 43 | /// // GitHub URL 44 | /// Some("https://github.com/near-examples/nft-tutorial".into()) 45 | /// # ; 46 | /// ``` 47 | /// ```rust,no_run 48 | /// # let link: Option = 49 | /// // IPFS CID 50 | /// Some("bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq".into()) 51 | /// # ; 52 | /// ``` 53 | pub link: Option, 54 | 55 | /// List of supported NEAR standards (NEPs) with their versions 56 | /// 57 | /// This field is an addition of **1.1.0** **NEP-330** revision 58 | /// 59 | /// ## Examples: 60 | /// 61 | /// This field will always include NEP-330 itself: 62 | /// ```rust,no_run 63 | /// # use near_api::types::contract::Standard; 64 | /// # let link: Vec = 65 | /// // this is always at least 1.1.0 66 | /// vec![Standard { standard: "nep330".into(), version: "1.1.0".into() }] 67 | /// # ; 68 | /// ``` 69 | /// ```rust,no_run 70 | /// # use near_api::types::contract::Standard; 71 | /// # let link: Vec = 72 | /// vec![Standard { standard: "nep330".into(), version: "1.2.0".into() }] 73 | /// # ; 74 | /// ``` 75 | // it's a guess it was added as 1.1.0 of nep330, [nep330 1.1.0 standard recording](https://www.youtube.com/watch?v=pBLN9UyE6AA) actually discusses nep351 76 | #[serde(default)] 77 | pub standards: Vec, 78 | 79 | /// Optional details that are required for formal contract WASM build reproducibility verification 80 | /// 81 | /// This field is an addition of **1.2.0** **NEP-330** revision 82 | pub build_info: Option, 83 | } 84 | 85 | /// NEAR Standard implementation descriptor following [NEP-330](https://github.com/near/NEPs/blob/master/neps/nep-0330.md) 86 | #[derive(Debug, Clone, PartialEq, Default, Eq, Serialize, Deserialize)] 87 | pub struct Standard { 88 | /// Standard name in lowercase NEP format 89 | /// 90 | /// ## Examples: 91 | /// 92 | /// ```rust,no_run 93 | /// # let standard: String = 94 | /// // for fungible tokens 95 | /// "nep141".into() 96 | /// # ; 97 | /// ``` 98 | pub standard: String, 99 | 100 | /// Implemented standard version using semantic versioning 101 | /// 102 | /// ## Examples: 103 | /// 104 | /// ```rust,no_run 105 | /// # let version: String = 106 | /// // for initial release 107 | /// "1.0.0".into() 108 | /// # ; 109 | /// ``` 110 | pub version: String, 111 | } 112 | 113 | mod build_info { 114 | use serde::{Deserialize, Serialize}; 115 | 116 | #[derive(Debug, Clone, PartialEq, Default, Eq, Serialize, Deserialize)] 117 | /// Defines all required details for formal WASM build reproducibility verification 118 | /// according to [**NEP-330 standard 1.3.0 revision**](https://github.com/near/NEPs/blob/master/neps/nep-0330.md) 119 | pub struct BuildInfo { 120 | /// Reference to a reproducible build environment docker image 121 | /// 122 | /// ## Examples: 123 | /// 124 | /// ```rust,no_run 125 | /// # let build_environment: String = 126 | /// "sourcescan/cargo-near:0.13.3-rust-1.84.0@sha256:722198ddb92d1b82cbfcd3a4a9f7fba6fd8715f4d0b5fb236d8725c4883f97de".into() 127 | /// # ; 128 | /// ``` 129 | pub build_environment: String, 130 | /// The exact command that was used to build the contract, with all the flags 131 | /// 132 | /// ## Examples: 133 | /// 134 | /// ```rust,no_run 135 | /// # let build_command: Vec = 136 | /// vec![ 137 | /// "cargo".into(), 138 | /// "near".into(), 139 | /// "build".into(), 140 | /// "non-reproducible-wasm".into(), 141 | /// "--locked".into() 142 | /// ] 143 | /// # ; 144 | /// ``` 145 | pub build_command: Vec, 146 | /// Relative path to contract crate within the source code 147 | /// 148 | /// ## Examples: 149 | /// 150 | /// ```rust,no_run 151 | /// # let contract_path: String = 152 | /// "near/omni-prover/wormhole-omni-prover-proxy".into() 153 | /// # ; 154 | /// ``` 155 | /// ```rust,no_run 156 | /// # let contract_path: String = 157 | /// // root of a repo 158 | /// "".into() 159 | /// # ; 160 | /// ``` 161 | pub contract_path: String, 162 | /// Reference to the source code snapshot that was used to build the contract 163 | /// 164 | /// ## Examples: 165 | /// 166 | /// ```rust,no_run 167 | /// # let source_code_snapshot: String = 168 | /// "git+https://github.com/org/repo?rev=8d8a8a0fe86a1d8eb3bce45f04ab1a65fecf5a1b".into() 169 | /// # ; 170 | /// ``` 171 | pub source_code_snapshot: String, 172 | /// A path within the build environment, where the result WASM binary has been put 173 | /// during build. 174 | /// This should be a subpath of `/home/near/code` 175 | /// 176 | /// This field is an addition of **1.3.0** **NEP-330** revision 177 | /// 178 | /// ## Examples: 179 | /// 180 | /// ```rust,no_run 181 | /// # let output_wasm_path: Option = 182 | /// Some("/home/near/code/target/near/simple_package.wasm".into()) 183 | /// # ; 184 | /// ``` 185 | pub output_wasm_path: Option, 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use near_primitives::types::BlockHeight; 4 | use reqwest::header::InvalidHeaderValue; 5 | 6 | use crate::errors::CryptoHashError; 7 | 8 | pub mod contract; 9 | pub mod reference; 10 | pub mod signed_delegate_action; 11 | pub mod stake; 12 | pub mod storage; 13 | pub mod tokens; 14 | pub mod transactions; 15 | 16 | /// A wrapper around a generic query result that includes the block height and block hash 17 | /// at which the query was executed 18 | #[derive( 19 | Debug, 20 | Clone, 21 | serde::Serialize, 22 | serde::Deserialize, 23 | borsh::BorshDeserialize, 24 | borsh::BorshSerialize, 25 | )] 26 | pub struct Data { 27 | /// The data returned by the query 28 | pub data: T, 29 | /// The block height at which the query was executed 30 | pub block_height: BlockHeight, 31 | /// The block hash at which the query was executed 32 | pub block_hash: CryptoHash, 33 | } 34 | 35 | impl Data { 36 | pub fn map(self, f: impl FnOnce(T) -> U) -> Data { 37 | Data { 38 | data: f(self.data), 39 | block_height: self.block_height, 40 | block_hash: self.block_hash, 41 | } 42 | } 43 | } 44 | 45 | /// A wrapper around [near_jsonrpc_client::auth::ApiKey] 46 | /// 47 | /// This type is used to authenticate requests to the RPC node 48 | /// 49 | /// ## Creating an API key 50 | /// 51 | /// ``` 52 | /// use near_api::types::ApiKey; 53 | /// use std::str::FromStr; 54 | /// 55 | /// # async fn example() -> Result<(), Box> { 56 | /// let api_key = ApiKey::from_str("your_api_key")?; 57 | /// # Ok(()) 58 | /// # } 59 | /// ``` 60 | #[derive(Eq, Hash, Clone, Debug, PartialEq)] 61 | pub struct ApiKey(near_jsonrpc_client::auth::ApiKey); 62 | 63 | impl From for near_jsonrpc_client::auth::ApiKey { 64 | fn from(api_key: ApiKey) -> Self { 65 | api_key.0 66 | } 67 | } 68 | 69 | impl std::fmt::Display for ApiKey { 70 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 71 | write!(f, "{}", self.0.to_str().map_err(|_| std::fmt::Error)?) 72 | } 73 | } 74 | 75 | impl std::str::FromStr for ApiKey { 76 | type Err = InvalidHeaderValue; 77 | 78 | fn from_str(api_key: &str) -> Result { 79 | Ok(Self(near_jsonrpc_client::auth::ApiKey::new(api_key)?)) 80 | } 81 | } 82 | 83 | impl serde::ser::Serialize for ApiKey { 84 | fn serialize(&self, serializer: S) -> Result 85 | where 86 | S: serde::ser::Serializer, 87 | { 88 | serializer.serialize_str(self.0.to_str().map_err(serde::ser::Error::custom)?) 89 | } 90 | } 91 | 92 | impl<'de> serde::de::Deserialize<'de> for ApiKey { 93 | fn deserialize(deserializer: D) -> Result 94 | where 95 | D: serde::de::Deserializer<'de>, 96 | { 97 | String::deserialize(deserializer)? 98 | .parse() 99 | .map_err(|err: InvalidHeaderValue| serde::de::Error::custom(err.to_string())) 100 | } 101 | } 102 | 103 | fn from_base58(s: &str) -> Result, bs58::decode::Error> { 104 | bs58::decode(s).into_vec() 105 | } 106 | 107 | /// A type that represents a hash of the data. 108 | /// 109 | /// This type is copy of the [near_primitives::hash::CryptoHash] 110 | /// as part of the [decoupling initiative](https://github.com/near/near-api-rs/issues/5) 111 | #[derive( 112 | Copy, 113 | Clone, 114 | Default, 115 | Hash, 116 | Eq, 117 | PartialEq, 118 | Ord, 119 | PartialOrd, 120 | serde::Serialize, 121 | serde::Deserialize, 122 | borsh::BorshDeserialize, 123 | borsh::BorshSerialize, 124 | )] 125 | pub struct CryptoHash(pub [u8; 32]); 126 | 127 | impl std::str::FromStr for CryptoHash { 128 | type Err = CryptoHashError; 129 | 130 | fn from_str(s: &str) -> Result { 131 | let bytes = from_base58(s)?; 132 | Self::try_from(bytes) 133 | } 134 | } 135 | 136 | impl TryFrom<&[u8]> for CryptoHash { 137 | type Error = CryptoHashError; 138 | 139 | fn try_from(bytes: &[u8]) -> Result { 140 | if bytes.len() != 32 { 141 | return Err(CryptoHashError::IncorrectHashLength(bytes.len())); 142 | } 143 | let mut buf = [0; 32]; 144 | buf.copy_from_slice(bytes); 145 | Ok(Self(buf)) 146 | } 147 | } 148 | 149 | impl TryFrom> for CryptoHash { 150 | type Error = CryptoHashError; 151 | 152 | fn try_from(v: Vec) -> Result { 153 | >::try_from(v.as_ref()) 154 | } 155 | } 156 | 157 | impl std::fmt::Debug for CryptoHash { 158 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 159 | write!(f, "{}", self) 160 | } 161 | } 162 | 163 | impl std::fmt::Display for CryptoHash { 164 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 165 | std::fmt::Display::fmt(&bs58::encode(self.0).into_string(), f) 166 | } 167 | } 168 | 169 | impl From for CryptoHash { 170 | fn from(hash: near_primitives::hash::CryptoHash) -> Self { 171 | Self(hash.0) 172 | } 173 | } 174 | 175 | impl From for near_primitives::hash::CryptoHash { 176 | fn from(hash: CryptoHash) -> Self { 177 | Self(hash.0) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/types/reference.rs: -------------------------------------------------------------------------------- 1 | // Source: 2 | 3 | use crate::types::CryptoHash; 4 | use near_primitives::types::{BlockHeight, EpochId}; 5 | 6 | /// A reference to a specific block. This type is used to specify the block for most queries. 7 | /// 8 | /// It represents the finality of a transaction or block in which transaction is included in. For more info 9 | /// go to the [NEAR finality](https://docs.near.org/docs/concepts/transaction#finality) docs. 10 | #[derive(Clone, Debug)] 11 | #[non_exhaustive] 12 | pub enum Reference { 13 | /// Optimistic finality. The latest block recorded on the node that responded to our query 14 | /// (<1 second delay after the transaction is submitted). 15 | Optimistic, 16 | /// Near-final finality. Similarly to `Final` finality, but delay should be roughly 1 second. 17 | DoomSlug, 18 | /// Final finality. The block that has been validated on at least 66% of the nodes in the 19 | /// network. (At max, should be 2 second delay after the transaction is submitted.) 20 | Final, 21 | /// Reference to a specific block. 22 | AtBlock(BlockHeight), 23 | /// Reference to a specific block hash. 24 | AtBlockHash(CryptoHash), 25 | } 26 | 27 | impl From for near_primitives::types::BlockReference { 28 | fn from(value: Reference) -> Self { 29 | match value { 30 | Reference::Optimistic => near_primitives::types::Finality::None.into(), 31 | Reference::DoomSlug => near_primitives::types::Finality::DoomSlug.into(), 32 | Reference::Final => near_primitives::types::Finality::Final.into(), 33 | Reference::AtBlock(block_height) => { 34 | near_primitives::types::BlockId::Height(block_height).into() 35 | } 36 | Reference::AtBlockHash(block_hash) => { 37 | near_primitives::types::BlockId::Hash(block_hash.into()).into() 38 | } 39 | } 40 | } 41 | } 42 | 43 | /// A reference to a specific epoch. This type is used to specify the epoch for some queries. 44 | #[derive(Clone, Debug)] 45 | #[non_exhaustive] 46 | pub enum EpochReference { 47 | /// Reference to a specific Epoch Id 48 | AtEpoch(CryptoHash), 49 | /// Reference to an epoch at a specific block height. 50 | AtBlock(BlockHeight), 51 | /// Reference to an epoch at a specific block hash. 52 | AtBlockHash(CryptoHash), 53 | /// Latest epoch on the node 54 | Latest, 55 | } 56 | 57 | impl From for near_primitives::types::EpochReference { 58 | fn from(value: EpochReference) -> Self { 59 | match value { 60 | EpochReference::AtBlock(block_height) => { 61 | Self::BlockId(near_primitives::types::BlockId::Height(block_height)) 62 | } 63 | EpochReference::AtBlockHash(block_hash) => { 64 | Self::BlockId(near_primitives::types::BlockId::Hash(block_hash.into())) 65 | } 66 | EpochReference::AtEpoch(epoch) => Self::EpochId(EpochId(epoch.into())), 67 | EpochReference::Latest => Self::Latest, 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/types/signed_delegate_action.rs: -------------------------------------------------------------------------------- 1 | use near_primitives::borsh; 2 | 3 | use crate::errors::SignedDelegateActionError; 4 | 5 | /// A wrapper around [near_primitives::action::delegate::SignedDelegateAction] that allows for easy serialization and deserialization as base64 string 6 | /// 7 | /// The type implements [std::str::FromStr] and [std::fmt::Display] to serialize and deserialize the type as base64 string 8 | #[derive(Debug, Clone)] 9 | pub struct SignedDelegateActionAsBase64 { 10 | /// The inner signed delegate action 11 | pub inner: near_primitives::action::delegate::SignedDelegateAction, 12 | } 13 | 14 | impl std::str::FromStr for SignedDelegateActionAsBase64 { 15 | type Err = SignedDelegateActionError; 16 | fn from_str(s: &str) -> Result { 17 | Ok(Self { 18 | inner: borsh::from_slice( 19 | &near_primitives::serialize::from_base64(s) 20 | .map_err(|_| SignedDelegateActionError::Base64DecodingError)?, 21 | )?, 22 | }) 23 | } 24 | } 25 | 26 | impl std::fmt::Display for SignedDelegateActionAsBase64 { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | let base64_signed_delegate_action = near_primitives::serialize::to_base64( 29 | &borsh::to_vec(&self.inner) 30 | .expect("Signed Delegate Action serialization to borsh is not expected to fail"), 31 | ); 32 | write!(f, "{}", base64_signed_delegate_action) 33 | } 34 | } 35 | 36 | impl From 37 | for SignedDelegateActionAsBase64 38 | { 39 | fn from(value: near_primitives::action::delegate::SignedDelegateAction) -> Self { 40 | Self { inner: value } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/types/stake.rs: -------------------------------------------------------------------------------- 1 | use near_token::NearToken; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Aggregate information about the staking pool. 5 | /// 6 | /// The type is related to the [StakingPool](https://github.com/near/core-contracts/tree/master/staking-pool) smart contract. 7 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 8 | pub struct StakingPoolInfo { 9 | /// The validator that is running the pool. 10 | pub validator_id: near_primitives::types::AccountId, 11 | /// The fee that is taken by the pool contract. 12 | pub fee: Option, 13 | /// The number of delegators on the pool. 14 | pub delegators: Option, 15 | /// The total staked balance on the pool (by all delegators). 16 | pub stake: NearToken, 17 | } 18 | 19 | /// The reward fee that is taken by the pool contract. 20 | /// 21 | /// This represents the percentage of the reward that is taken by the pool contract. 22 | /// The type is a part of the [StakingPool](https://github.com/near/core-contracts/tree/master/staking-pool) interface 23 | /// 24 | /// The fraction is equal to numerator/denominator, e.g. 3/1000 = 0.3% 25 | #[derive(Debug, Clone, PartialEq, Default, Eq, Serialize, Deserialize)] 26 | pub struct RewardFeeFraction { 27 | /// The numerator of the fraction. 28 | pub numerator: u32, 29 | /// The denominator of the fraction. 30 | pub denominator: u32, 31 | } 32 | 33 | /// The total user balance on a pool contract 34 | /// 35 | /// The type is related to the [StakingPool](https://github.com/near/core-contracts/tree/master/staking-pool) smart contract. 36 | #[derive(Debug, Clone, PartialEq, Default, Eq, Serialize, Deserialize)] 37 | pub struct UserStakeBalance { 38 | /// The balance that currently is staked. The user can't withdraw this balance until `unstake` is called 39 | /// and withdraw period is over. 40 | pub staked: NearToken, 41 | /// The balance that is not staked. The user can start withdrawing this balance. Some pools 42 | /// have a withdraw period. 43 | pub unstaked: NearToken, 44 | /// The total balance of the user on a contract (staked + unstaked) 45 | pub total: NearToken, 46 | } 47 | -------------------------------------------------------------------------------- /src/types/storage.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::NearToken; 2 | use serde::de::{Deserialize, Deserializer}; 3 | 4 | /// A type that represents the storage balance. Please note that this type is not part of the NEP-145 standard. 5 | /// This type provides a more detailed view of the storage balance on the contract. 6 | /// 7 | /// [StorageBalanceInternal] is a [NEP-145](https://github.com/near/NEPs/blob/master/neps/nep-0145.md) standard type. 8 | /// This type is used internally to parse the storage balance from the contract and 9 | /// to convert it into the [StorageBalance] type. 10 | /// 11 | /// As a storing data on-chain requires storage staking, the contracts require users to deposit NEAR to store user-rel. 12 | #[derive(Debug, Clone)] 13 | pub struct StorageBalance { 14 | /// The available balance that might be used for storage. 15 | /// 16 | /// The user can withdraw this balance from the contract. 17 | pub available: NearToken, 18 | /// The total user balance on the contract for storage. 19 | /// 20 | /// This is a sum of the `available` and `locked` balances. 21 | pub total: NearToken, 22 | 23 | /// The storage deposit that is locked for the account 24 | /// 25 | /// The user can unlock some funds by removing the data from the contract. 26 | /// Though, it's contract-specific on how much can be unlocked. 27 | pub locked: NearToken, 28 | } 29 | 30 | /// Used internally to parse the storage balance from the contract and 31 | /// to convert it into the [StorageBalance] type. 32 | /// 33 | /// This type is a part of the [NEP-145](https://github.com/near/NEPs/blob/master/neps/nep-0145.md) standard. 34 | #[derive(Debug, Clone, serde::Deserialize)] 35 | pub struct StorageBalanceInternal { 36 | #[serde(deserialize_with = "parse_u128_string")] 37 | pub available: NearToken, 38 | #[serde(deserialize_with = "parse_u128_string")] 39 | pub total: NearToken, 40 | } 41 | 42 | fn parse_u128_string<'de, D>(deserializer: D) -> Result 43 | where 44 | D: Deserializer<'de>, 45 | { 46 | ::deserialize(deserializer)? 47 | .parse::() 48 | .map(NearToken::from_yoctonear) 49 | .map_err(serde::de::Error::custom) 50 | } 51 | -------------------------------------------------------------------------------- /src/types/tokens.rs: -------------------------------------------------------------------------------- 1 | use near_token::NearToken; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::errors::DecimalNumberParsingError; 5 | 6 | /// Static instance of [FTBalance] for USDT token with correct decimals and symbol. 7 | pub const USDT_BALANCE: FTBalance = FTBalance::with_decimals_and_symbol(6, "USDT"); 8 | /// Static instance of [FTBalance] for USDC token with correct decimals and symbol. 9 | pub const USDC_BALANCE: FTBalance = FTBalance::with_decimals_and_symbol(6, "USDC"); 10 | /// Static instance of [FTBalance] for wNEAR token with correct decimals and symbol. 11 | pub const W_NEAR_BALANCE: FTBalance = FTBalance::with_decimals_and_symbol(24, "wNEAR"); 12 | 13 | /// The cost of storage per byte in NEAR. 14 | pub const STORAGE_COST_PER_BYTE: NearToken = NearToken::from_yoctonear(10u128.pow(19)); 15 | 16 | /// A helper type that represents the fungible token balance with a given precision. 17 | /// 18 | /// The type is created to simplify the usage of fungible tokens in similar way as the [NearToken] type does. 19 | /// 20 | /// The symbol is used only for display purposes. 21 | /// 22 | /// The type has static instances for some of the most popular tokens with correct decimals and symbol. 23 | /// * [USDT_BALANCE] - USDT token with 6 decimals 24 | /// * [USDC_BALANCE] - USDC token with 6 decimals 25 | /// * [W_NEAR_BALANCE] - wNEAR token with 24 decimals 26 | /// 27 | /// # Examples 28 | /// 29 | /// ## Defining 2.5 USDT 30 | /// ```rust 31 | /// use near_api::FTBalance; 32 | /// 33 | /// let usdt_balance = FTBalance::with_decimals(6).with_float_str("2.5").unwrap(); 34 | /// 35 | /// assert_eq!(usdt_balance.amount(), 2_500_000); 36 | /// ``` 37 | /// 38 | /// ## Defining 3 USDT using smaller precision 39 | /// ```rust 40 | /// use near_api::FTBalance; 41 | /// 42 | /// let usdt = FTBalance::with_decimals(6); 43 | /// 44 | /// let usdt_balance = usdt.with_amount(3 * 10u128.pow(6)); 45 | /// 46 | /// assert_eq!(usdt_balance, usdt.with_whole_amount(3)); 47 | /// ``` 48 | /// 49 | /// ## Defining 3 wETH using 18 decimals 50 | /// ```rust 51 | /// use near_api::FTBalance; 52 | /// 53 | /// let weth = FTBalance::with_decimals_and_symbol(18, "wETH"); 54 | /// let weth_balance = weth.with_whole_amount(3); 55 | /// 56 | /// assert_eq!(weth_balance, weth.with_amount(3 * 10u128.pow(18))); 57 | /// ``` 58 | #[derive(Debug, Clone, PartialEq, Default, Eq, Serialize, Deserialize)] 59 | pub struct FTBalance { 60 | amount: u128, 61 | decimals: u8, 62 | symbol: &'static str, 63 | } 64 | 65 | impl FTBalance { 66 | /// Creates a new [FTBalance] with a given precision. 67 | /// 68 | /// The balance is initialized to 0. 69 | pub const fn with_decimals(decimals: u8) -> Self { 70 | Self { 71 | amount: 0, 72 | decimals, 73 | symbol: "FT", 74 | } 75 | } 76 | 77 | /// Creates a new [FTBalance] with a given precision and symbol. 78 | /// 79 | /// The balance is initialized to 0. 80 | pub const fn with_decimals_and_symbol(decimals: u8, symbol: &'static str) -> Self { 81 | Self { 82 | amount: 0, 83 | decimals, 84 | symbol, 85 | } 86 | } 87 | 88 | /// Stores the given amount without any transformations. 89 | /// 90 | /// The [NearToken] equivalent to this method is [NearToken::from_yoctonear]. 91 | /// 92 | /// ## Example 93 | /// ```rust 94 | /// use near_api::FTBalance; 95 | /// 96 | /// let usdt_balance = FTBalance::with_decimals(6).with_amount(2_500_000); 97 | /// assert_eq!(usdt_balance.amount(), 2_500_000); 98 | /// assert_eq!(usdt_balance.to_whole(), 2); 99 | /// ``` 100 | pub const fn with_amount(&self, amount: u128) -> Self { 101 | Self { 102 | amount, 103 | decimals: self.decimals, 104 | symbol: self.symbol, 105 | } 106 | } 107 | 108 | /// Stores the number as an integer token value utilizing the given precision. 109 | /// 110 | /// The [NearToken] equivalent to this method is [NearToken::from_near]. 111 | /// 112 | /// ## Example 113 | /// ```rust 114 | /// use near_api::FTBalance; 115 | /// 116 | /// let usdt_balance = FTBalance::with_decimals(6).with_whole_amount(3); 117 | /// assert_eq!(usdt_balance.amount(), 3 * 10u128.pow(6)); 118 | /// assert_eq!(usdt_balance.to_whole(), 3); 119 | /// ``` 120 | pub const fn with_whole_amount(&self, amount: u128) -> Self { 121 | Self { 122 | amount: amount * 10u128.pow(self.decimals as u32), 123 | decimals: self.decimals, 124 | symbol: self.symbol, 125 | } 126 | } 127 | 128 | /// Parses float string and stores the value in defined precision. 129 | /// 130 | /// # Examples 131 | /// 132 | /// ## Defining 2.5 USDT 133 | /// ```rust 134 | /// use near_api::FTBalance; 135 | /// 136 | /// let usdt_balance = FTBalance::with_decimals(6).with_float_str("2.515").unwrap(); 137 | /// 138 | /// assert_eq!(usdt_balance.amount(), 2_515_000); 139 | /// ``` 140 | pub fn with_float_str(&self, float_str: &str) -> Result { 141 | crate::common::utils::parse_decimal_number(float_str, 10u128.pow(self.decimals as u32)) 142 | .map(|amount| self.with_amount(amount)) 143 | } 144 | 145 | /// Returns the amount without any transformations. 146 | /// 147 | /// The [NearToken] equivalent to this method is [NearToken::as_yoctonear]. 148 | pub const fn amount(&self) -> u128 { 149 | self.amount 150 | } 151 | 152 | /// Returns the amount as a whole number in the integer precision. 153 | /// 154 | /// The method rounds down the fractional part, so 2.5 USDT will be 2. 155 | /// 156 | /// The [NearToken] equivalent to this method is [NearToken::as_near]. 157 | pub const fn to_whole(&self) -> u128 { 158 | self.amount / 10u128.pow(self.decimals as u32) 159 | } 160 | 161 | /// Returns the number of decimals used by the token. 162 | pub const fn decimals(&self) -> u8 { 163 | self.decimals 164 | } 165 | } 166 | 167 | impl PartialOrd for FTBalance { 168 | fn partial_cmp(&self, other: &Self) -> Option { 169 | if self.decimals != other.decimals || self.symbol != other.symbol { 170 | return None; 171 | } 172 | 173 | Some(self.amount.cmp(&other.amount)) 174 | } 175 | } 176 | 177 | impl std::fmt::Display for FTBalance { 178 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 179 | let whole_part = self.to_whole(); 180 | let fractional_part = self.amount % 10u128.pow(self.decimals as u32); 181 | 182 | let fractional_part_str = format!( 183 | "{:0width$}", 184 | fractional_part, 185 | width = self.decimals as usize 186 | ); 187 | let fractional_part_str = fractional_part_str.trim_end_matches('0'); 188 | 189 | if fractional_part_str.is_empty() { 190 | return write!(f, "{} {}", whole_part, self.symbol); 191 | } 192 | 193 | write!(f, "{}.{} {}", whole_part, fractional_part_str, self.symbol) 194 | } 195 | } 196 | 197 | /// Account balance on the NEAR blockchain. 198 | /// 199 | /// This balance doesn't include staked NEAR tokens or storage 200 | #[derive(Debug, Clone, PartialEq, Default, Eq, Serialize, Deserialize)] 201 | pub struct UserBalance { 202 | /// The total amount of NEAR tokens in the account. 203 | /// 204 | /// Please note that this is the total amount of NEAR tokens in the account, not the amount available for use. 205 | pub total: NearToken, 206 | /// The amount of NEAR tokens locked in the account for storage usage. 207 | /// 208 | /// The storage lock equal to [Self::storage_usage] * [STORAGE_COST_PER_BYTE] 209 | pub storage_locked: NearToken, 210 | /// The storage usage by the account in bytes. 211 | pub storage_usage: u64, 212 | /// The amount of NEAR tokens staked on a protocol level. 213 | /// Applicable for staking pools only in 99.99% of the cases. 214 | /// 215 | /// The PoS allows particular users to stake funds to become a validator, but the protocol itself 216 | /// doesn't allow other users to delegate tokens to the validator. 217 | /// This is why, the [NEP-27](https://github.com/near/core-contracts/tree/master/staking-pool) defines a Staking Pool smart contract 218 | /// that allows other users to delegate tokens to the validator. 219 | /// 220 | /// Even though, the user can stake and become validator itself, it's highly unlikely and this field will be 0 221 | /// for almost all the users, and not 0 for StakingPool contracts. 222 | /// 223 | /// Please note that this is not related to your delegations into the staking pools. 224 | /// To get your delegation information in the staking pools, use [crate::Delegation] 225 | pub locked: NearToken, 226 | } 227 | 228 | #[cfg(test)] 229 | mod tests { 230 | use super::FTBalance; 231 | 232 | #[test] 233 | fn ft_balance_default() { 234 | assert_eq!( 235 | FTBalance::with_decimals(5).with_whole_amount(5).amount(), 236 | 500000 237 | ); 238 | assert_eq!(FTBalance::with_decimals(5).with_amount(5).amount(), 5); 239 | 240 | assert_eq!( 241 | FTBalance::with_decimals(5).with_whole_amount(5).to_whole(), 242 | 5 243 | ); 244 | } 245 | 246 | #[test] 247 | fn ft_balance_str() { 248 | assert_eq!( 249 | FTBalance::with_decimals(5) 250 | .with_float_str("5") 251 | .unwrap() 252 | .amount(), 253 | 500000 254 | ); 255 | assert_eq!( 256 | FTBalance::with_decimals(5) 257 | .with_float_str("5.00001") 258 | .unwrap() 259 | .amount(), 260 | 500001 261 | ); 262 | assert_eq!( 263 | FTBalance::with_decimals(5) 264 | .with_float_str("5.55") 265 | .unwrap() 266 | .amount(), 267 | 555000 268 | ); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/types/transactions.rs: -------------------------------------------------------------------------------- 1 | use near_primitives::{action::Action, types::AccountId}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// An internal type that represents unsigned transaction. 5 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 6 | pub struct PrepopulateTransaction { 7 | /// The account that will sign the transaction. 8 | pub signer_id: AccountId, 9 | /// The account that will receive the transaction 10 | pub receiver_id: AccountId, 11 | /// The actions that will be executed by the transaction. 12 | pub actions: Vec, 13 | } 14 | -------------------------------------------------------------------------------- /tests/account.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | use near_primitives::{account::AccessKeyPermission, views::AccessKeyPermissionView}; 3 | use signer::generate_secret_key; 4 | 5 | #[tokio::test] 6 | async fn create_and_delete_account() { 7 | let network = near_workspaces::sandbox().await.unwrap(); 8 | let account = network.dev_create_account().await.unwrap(); 9 | let network: NetworkConfig = NetworkConfig::from(network); 10 | 11 | let new_account: AccountId = format!("{}.{}", "bob", account.id()).parse().unwrap(); 12 | let secret = generate_secret_key().unwrap(); 13 | Account::create_account(new_account.clone()) 14 | .fund_myself(account.id().clone(), NearToken::from_near(1)) 15 | .public_key(secret.public_key()) 16 | .unwrap() 17 | .with_signer(Signer::new(Signer::from_workspace(&account)).unwrap()) 18 | .send_to(&network) 19 | .await 20 | .unwrap() 21 | .assert_success(); 22 | 23 | let balance_before_del = Tokens::account(new_account.clone()) 24 | .near_balance() 25 | .fetch_from(&network) 26 | .await 27 | .unwrap(); 28 | 29 | assert_eq!(balance_before_del.total.as_near(), 1); 30 | 31 | Account(account.id().clone()) 32 | .delete_account_with_beneficiary(new_account.clone()) 33 | .with_signer(Signer::new(Signer::from_workspace(&account)).unwrap()) 34 | .send_to(&network) 35 | .await 36 | .unwrap() 37 | .assert_success(); 38 | 39 | Tokens::account(account.id().clone()) 40 | .near_balance() 41 | .fetch_from(&network) 42 | .await 43 | .expect_err("Shouldn't exist"); 44 | 45 | // TODO: why do we need a sleep to wait for beneficiary transfer? 46 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 47 | 48 | let balance_after_del = Tokens::account(new_account.clone()) 49 | .near_balance() 50 | .fetch_from(&network) 51 | .await 52 | .unwrap(); 53 | assert!(balance_after_del.total > balance_before_del.total); 54 | } 55 | 56 | #[tokio::test] 57 | async fn transfer_funds() { 58 | let network = near_workspaces::sandbox().await.unwrap(); 59 | let alice = network.dev_create_account().await.unwrap(); 60 | let bob = network.dev_create_account().await.unwrap(); 61 | let network: NetworkConfig = NetworkConfig::from(network); 62 | 63 | Tokens::account(alice.id().clone()) 64 | .send_to(bob.id().clone()) 65 | .near(NearToken::from_near(50)) 66 | .with_signer(Signer::new(Signer::from_workspace(&alice)).unwrap()) 67 | .send_to(&network) 68 | .await 69 | .unwrap() 70 | .assert_success(); 71 | 72 | let alice_balance = Tokens::account(alice.id().clone()) 73 | .near_balance() 74 | .fetch_from(&network) 75 | .await 76 | .unwrap(); 77 | 78 | let bob_balance = Tokens::account(bob.id().clone()) 79 | .near_balance() 80 | .fetch_from(&network) 81 | .await 82 | .unwrap(); 83 | 84 | // it's actually 49.99 because of the fee 85 | assert_eq!(alice_balance.total.as_near(), 49); 86 | assert_eq!(bob_balance.total.as_near(), 150); 87 | } 88 | 89 | #[tokio::test] 90 | async fn access_key_management() { 91 | let network = near_workspaces::sandbox().await.unwrap(); 92 | let alice = network.dev_create_account().await.unwrap(); 93 | let network: NetworkConfig = NetworkConfig::from(network); 94 | 95 | let alice_acc = Account(alice.id().clone()); 96 | 97 | let keys = alice_acc.list_keys().fetch_from(&network).await.unwrap(); 98 | assert_eq!(keys.keys.len(), 1); 99 | 100 | let secret = generate_secret_key().unwrap(); 101 | 102 | alice_acc 103 | .add_key(AccessKeyPermission::FullAccess, secret.public_key()) 104 | .with_signer(Signer::new(Signer::from_workspace(&alice)).unwrap()) 105 | .send_to(&network) 106 | .await 107 | .unwrap() 108 | .assert_success(); 109 | 110 | let keys = alice_acc.list_keys().fetch_from(&network).await.unwrap(); 111 | assert_eq!(keys.keys.len(), 2); 112 | 113 | let new_key_info = alice_acc 114 | .access_key(secret.public_key()) 115 | .fetch_from(&network) 116 | .await 117 | .unwrap(); 118 | 119 | assert_eq!( 120 | new_key_info.data.permission, 121 | AccessKeyPermissionView::FullAccess 122 | ); 123 | 124 | alice_acc 125 | .delete_key(secret.public_key()) 126 | .with_signer(Signer::new(Signer::from_workspace(&alice)).unwrap()) 127 | .send_to(&network) 128 | .await 129 | .unwrap() 130 | .assert_success(); 131 | 132 | let keys = alice_acc.list_keys().fetch_from(&network).await.unwrap(); 133 | 134 | assert_eq!(keys.keys.len(), 1); 135 | 136 | alice_acc 137 | .access_key(secret.public_key()) 138 | .fetch_from(&network) 139 | .await 140 | .expect_err("Shouldn't exist"); 141 | 142 | for _ in 0..10 { 143 | let secret = generate_secret_key().unwrap(); 144 | alice_acc 145 | .add_key(AccessKeyPermission::FullAccess, secret.public_key()) 146 | .with_signer(Signer::new(Signer::from_workspace(&alice)).unwrap()) 147 | .send_to(&network) 148 | .await 149 | .unwrap() 150 | .assert_success(); 151 | } 152 | 153 | let keys = alice_acc.list_keys().fetch_from(&network).await.unwrap(); 154 | 155 | assert_eq!(keys.keys.len(), 11); 156 | 157 | alice_acc 158 | .delete_keys(keys.keys.into_iter().map(|k| k.public_key).collect()) 159 | .with_signer(Signer::new(Signer::from_workspace(&alice)).unwrap()) 160 | .send_to(&network) 161 | .await 162 | .unwrap() 163 | .assert_success(); 164 | 165 | let keys = alice_acc.list_keys().fetch_from(&network).await.unwrap(); 166 | assert_eq!(keys.keys.len(), 0); 167 | } 168 | -------------------------------------------------------------------------------- /tests/contract.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | 3 | use serde_json::json; 4 | 5 | #[tokio::test] 6 | async fn contract_without_init_call() { 7 | let network = near_workspaces::sandbox().await.unwrap(); 8 | let account = network.dev_create_account().await.unwrap(); 9 | let network = NetworkConfig::from(network); 10 | 11 | Contract::deploy(account.id().clone()) 12 | .use_code(include_bytes!("../resources/counter.wasm").to_vec()) 13 | .without_init_call() 14 | .with_signer(Signer::new(Signer::from_workspace(&account)).unwrap()) 15 | .send_to(&network) 16 | .await 17 | .unwrap() 18 | .assert_success(); 19 | 20 | let contract = Contract(account.id().clone()); 21 | 22 | assert!(!contract 23 | .wasm() 24 | .fetch_from(&network) 25 | .await 26 | .unwrap() 27 | .data 28 | .code 29 | .is_empty()); 30 | 31 | assert!(contract 32 | .contract_source_metadata() 33 | .fetch_from(&network) 34 | .await 35 | .unwrap() 36 | .data 37 | .version 38 | .is_some()); 39 | 40 | let current_value: Data = contract 41 | .call_function("get_num", ()) 42 | .unwrap() 43 | .read_only() 44 | .fetch_from(&network) 45 | .await 46 | .unwrap(); 47 | assert_eq!(current_value.data, 0); 48 | 49 | contract 50 | .call_function("increment", ()) 51 | .unwrap() 52 | .transaction() 53 | .with_signer( 54 | account.id().clone(), 55 | Signer::new(Signer::from_workspace(&account)).unwrap(), 56 | ) 57 | .send_to(&network) 58 | .await 59 | .unwrap() 60 | .assert_success(); 61 | 62 | let current_value: Data = contract 63 | .call_function("get_num", ()) 64 | .unwrap() 65 | .read_only() 66 | .fetch_from(&network) 67 | .await 68 | .unwrap(); 69 | 70 | assert_eq!(current_value.data, 1); 71 | } 72 | 73 | #[tokio::test] 74 | async fn contract_with_init_call() { 75 | let network = near_workspaces::sandbox().await.unwrap(); 76 | let account = network.dev_create_account().await.unwrap(); 77 | let network = NetworkConfig::from(network); 78 | 79 | Contract::deploy(account.id().clone()) 80 | .use_code(include_bytes!("../resources/fungible_token.wasm").to_vec()) 81 | .with_init_call( 82 | "new_default_meta", 83 | json!({ 84 | "owner_id": account.id().to_string(), 85 | "total_supply": "1000000000000000000000000000" 86 | }), 87 | ) 88 | .unwrap() 89 | .with_signer(Signer::new(Signer::from_workspace(&account)).unwrap()) 90 | .send_to(&network) 91 | .await 92 | .unwrap() 93 | .assert_success(); 94 | 95 | let contract = Contract(account.id().clone()); 96 | 97 | assert!(!contract 98 | .wasm() 99 | .fetch_from(&network) 100 | .await 101 | .unwrap() 102 | .data 103 | .code 104 | .is_empty()); 105 | } 106 | -------------------------------------------------------------------------------- /tests/global_contracts.rs: -------------------------------------------------------------------------------- 1 | use near_api::*; 2 | 3 | use near_primitives::hash::hash; 4 | 5 | #[tokio::test] 6 | async fn deploy_global_contract_as_account_id_and_use_it() { 7 | let network = near_workspaces::sandbox().await.unwrap(); 8 | let global_contract = network.dev_create_account().await.unwrap(); 9 | let contract_acc = network.dev_create_account().await.unwrap(); 10 | let network = NetworkConfig::from(network); 11 | 12 | Contract::deploy_global_contract_code(include_bytes!("../resources/counter.wasm").to_vec()) 13 | .as_account_id(global_contract.id().clone()) 14 | .with_signer(Signer::new(Signer::from_workspace(&global_contract)).unwrap()) 15 | .send_to(&network) 16 | .await 17 | .unwrap() 18 | .assert_success(); 19 | 20 | Contract::deploy(contract_acc.id().clone()) 21 | .use_global_account_id(global_contract.id().clone()) 22 | .without_init_call() 23 | .with_signer(Signer::new(Signer::from_workspace(&contract_acc)).unwrap()) 24 | .send_to(&network) 25 | .await 26 | .unwrap() 27 | .assert_success(); 28 | 29 | let contract = Contract(contract_acc.id().clone()); 30 | 31 | assert!(!contract 32 | .wasm() 33 | .fetch_from(&network) 34 | .await 35 | .unwrap() 36 | .data 37 | .code 38 | .is_empty()); 39 | 40 | assert!(contract 41 | .contract_source_metadata() 42 | .fetch_from(&network) 43 | .await 44 | .unwrap() 45 | .data 46 | .version 47 | .is_some()); 48 | 49 | let current_value: Data = contract 50 | .call_function("get_num", ()) 51 | .unwrap() 52 | .read_only() 53 | .fetch_from(&network) 54 | .await 55 | .unwrap(); 56 | assert_eq!(current_value.data, 0); 57 | 58 | contract 59 | .call_function("increment", ()) 60 | .unwrap() 61 | .transaction() 62 | .with_signer( 63 | contract_acc.id().clone(), 64 | Signer::new(Signer::from_workspace(&contract_acc)).unwrap(), 65 | ) 66 | .send_to(&network) 67 | .await 68 | .unwrap() 69 | .assert_success(); 70 | 71 | let current_value: Data = contract 72 | .call_function("get_num", ()) 73 | .unwrap() 74 | .read_only() 75 | .fetch_from(&network) 76 | .await 77 | .unwrap(); 78 | 79 | assert_eq!(current_value.data, 1); 80 | } 81 | 82 | #[tokio::test] 83 | async fn deploy_global_contract_as_hash_and_use_it() { 84 | let network = near_workspaces::sandbox().await.unwrap(); 85 | let contract_acc = network.dev_create_account().await.unwrap(); 86 | let network = NetworkConfig::from(network); 87 | 88 | let code = include_bytes!("../resources/counter.wasm").to_vec(); 89 | let hash = hash(&code); 90 | 91 | Contract::deploy_global_contract_code(code.clone()) 92 | .as_hash() 93 | .with_signer( 94 | contract_acc.id().clone(), 95 | Signer::new(Signer::from_workspace(&contract_acc)).unwrap(), 96 | ) 97 | .send_to(&network) 98 | .await 99 | .unwrap() 100 | .assert_success(); 101 | 102 | Contract::deploy(contract_acc.id().clone()) 103 | .use_global_hash(hash.into()) 104 | .without_init_call() 105 | .with_signer(Signer::new(Signer::from_workspace(&contract_acc)).unwrap()) 106 | .send_to(&network) 107 | .await 108 | .unwrap() 109 | .assert_success(); 110 | 111 | let contract = Contract(contract_acc.id().clone()); 112 | 113 | assert!(!contract 114 | .wasm() 115 | .fetch_from(&network) 116 | .await 117 | .unwrap() 118 | .data 119 | .code 120 | .is_empty()); 121 | 122 | assert!(contract 123 | .contract_source_metadata() 124 | .fetch_from(&network) 125 | .await 126 | .unwrap() 127 | .data 128 | .version 129 | .is_some()); 130 | 131 | let current_value: Data = contract 132 | .call_function("get_num", ()) 133 | .unwrap() 134 | .read_only() 135 | .fetch_from(&network) 136 | .await 137 | .unwrap(); 138 | assert_eq!(current_value.data, 0); 139 | 140 | contract 141 | .call_function("increment", ()) 142 | .unwrap() 143 | .transaction() 144 | .with_signer( 145 | contract_acc.id().clone(), 146 | Signer::new(Signer::from_workspace(&contract_acc)).unwrap(), 147 | ) 148 | .send_to(&network) 149 | .await 150 | .unwrap() 151 | .assert_success(); 152 | 153 | let current_value: Data = contract 154 | .call_function("get_num", ()) 155 | .unwrap() 156 | .read_only() 157 | .fetch_from(&network) 158 | .await 159 | .unwrap(); 160 | 161 | assert_eq!(current_value.data, 1); 162 | } 163 | -------------------------------------------------------------------------------- /tests/multiple_tx_at_same_time_from_same-_user.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use futures::future::join_all; 4 | use near_api::*; 5 | 6 | use near_crypto::PublicKey; 7 | use near_primitives::account::AccessKeyPermission; 8 | use signer::generate_secret_key; 9 | 10 | #[tokio::test] 11 | async fn multiple_tx_at_same_time_from_same_key() { 12 | let network = near_workspaces::sandbox().await.unwrap(); 13 | let account = network.dev_create_account().await.unwrap(); 14 | 15 | let tmp_account = network.dev_create_account().await.unwrap(); 16 | let network = NetworkConfig::from(network); 17 | 18 | let start_nonce = Account(account.id().clone()) 19 | .access_key(Signer::from_workspace(&account).get_public_key().unwrap()) 20 | .fetch_from(&network) 21 | .await 22 | .unwrap() 23 | .data 24 | .nonce; 25 | 26 | let tx = (0..100).map(|i| { 27 | Tokens::account(account.id().clone()) 28 | .send_to(tmp_account.id().clone()) 29 | .near(NearToken::from_millinear(i)) 30 | }); 31 | let signer = Signer::new(Signer::from_workspace(&account)).unwrap(); 32 | let txs = join_all(tx.map(|t| t.with_signer(Arc::clone(&signer)).send_to(&network))) 33 | .await 34 | .into_iter() 35 | .collect::, _>>() 36 | .unwrap(); 37 | 38 | assert_eq!(txs.len(), 100); 39 | txs.iter().for_each(|a| a.assert_success()); 40 | 41 | let end_nonce = Account(account.id().clone()) 42 | .access_key(Signer::from_workspace(&account).get_public_key().unwrap()) 43 | .fetch_from(&network) 44 | .await 45 | .unwrap() 46 | .data 47 | .nonce; 48 | assert_eq!(end_nonce, start_nonce + 100); 49 | } 50 | 51 | #[tokio::test] 52 | async fn multiple_tx_at_same_time_from_different_keys() { 53 | let network = near_workspaces::sandbox().await.unwrap(); 54 | let account = network.dev_create_account().await.unwrap(); 55 | let tmp_account = network.dev_create_account().await.unwrap(); 56 | 57 | let network = NetworkConfig::from(network); 58 | 59 | let signer = Signer::new(Signer::from_workspace(&account)).unwrap(); 60 | 61 | let secret = generate_secret_key().unwrap(); 62 | Account(account.id().clone()) 63 | .add_key(AccessKeyPermission::FullAccess, secret.public_key()) 64 | .with_signer(signer.clone()) 65 | .send_to(&network) 66 | .await 67 | .unwrap() 68 | .assert_success(); 69 | 70 | signer 71 | .add_signer_to_pool(Signer::from_secret_key(secret.clone())) 72 | .await 73 | .unwrap(); 74 | 75 | let secret2 = generate_secret_key().unwrap(); 76 | Account(account.id().clone()) 77 | .add_key(AccessKeyPermission::FullAccess, secret2.public_key()) 78 | .with_signer(signer.clone()) 79 | .send_to(&network) 80 | .await 81 | .unwrap(); 82 | signer 83 | .add_signer_to_pool(Signer::from_secret_key(secret2.clone())) 84 | .await 85 | .unwrap(); 86 | 87 | let tx = (0..12).map(|i| { 88 | Tokens::account(account.id().clone()) 89 | .send_to(tmp_account.id().clone()) 90 | .near(NearToken::from_millinear(i)) 91 | }); 92 | let txs = join_all(tx.map(|t| t.with_signer(Arc::clone(&signer)).send_to(&network))) 93 | .await 94 | .into_iter() 95 | .collect::, _>>() 96 | .unwrap(); 97 | 98 | assert_eq!(txs.len(), 12); 99 | let mut hash_map = HashMap::new(); 100 | for tx in txs { 101 | tx.assert_success(); 102 | let public_key = tx.transaction.public_key; 103 | let count = hash_map.entry(public_key).or_insert(0); 104 | *count += 1; 105 | } 106 | 107 | let initial_key = account.secret_key().public_key(); 108 | let initial_key: PublicKey = initial_key.to_string().parse().unwrap(); 109 | assert_eq!(hash_map.len(), 3); 110 | assert_eq!(hash_map[&initial_key], 4); 111 | assert_eq!(hash_map[&secret2.public_key()], 4); 112 | assert_eq!(hash_map[&secret.public_key()], 4); 113 | } 114 | --------------------------------------------------------------------------------