diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dc74515..bc7d1e18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,9 @@ name: CI +permissions: + pull-requests: write + contents: write + on: push: branches: [ master ] @@ -47,74 +51,21 @@ jobs: - name: Run cargo fmt run: cargo fmt --all -- --check - publish: - runs-on: ubuntu-20.04 + release-plz: + runs-on: ubuntu-latest needs: [test, clippy, cargo-fmt] if: github.ref == 'refs/heads/master' - steps: - - name: Checkout Repository - uses: actions/checkout@v2 - with: - # fetch tags for cargo ws publish - # might be a simple `fetch-tags: true` option soon, see https://github.com/actions/checkout/pull/579 - fetch-depth: 0 - - - name: Setup - run: | - git config user.name github-actions - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - cargo install --git https://github.com/miraclx/cargo-workspaces --rev b2d49b9e575e29fd2395352e4d0df47def025039 cargo-workspaces - export GIT_PREVIOUS_TAG=$(git describe --tags --abbrev=0) - echo "GIT_PREVIOUS_TAG=${GIT_PREVIOUS_TAG}" >> $GITHUB_ENV - echo "[ pre run] current latest git tag is \"${GIT_PREVIOUS_TAG}\"" - - - name: Publish to crates.io and tag the commit - id: tag-and-publish - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: | - cargo ws publish --all --yes --exact \ - --skip-published --no-git-commit --allow-dirty \ - --tag-existing --tag-prefix 'v' \ - --tag-msg 'crates.io snapshot' --tag-msg '%{https://crates.io/crates/%n/%v}' \ - --no-individual-tags --no-git-push - export GIT_LATEST_TAG=$(git describe --tags --abbrev=0) - echo "GIT_LATEST_TAG=${GIT_LATEST_TAG}" >> $GITHUB_ENV - echo "[post run] current latest git tag is \"${GIT_LATEST_TAG}\"" - echo "::set-output name=tagged::$( [[ "$GIT_LATEST_TAG" == "$GIT_PREVIOUS_TAG" ]] && echo 0 || echo 1 )" - - # returning multi-line outputs gets truncated to include only the first line - # we have to escape the newline chars, runner auto unescapes them later - # https://github.community/t/set-output-truncates-multiline-strings/16852/3 - GIT_TAG_MESSAGE="$(git tag -l --format='%(body)' ${GIT_LATEST_TAG})" - GIT_TAG_MESSAGE="${GIT_TAG_MESSAGE//'%'/'%25'}" - GIT_TAG_MESSAGE="${GIT_TAG_MESSAGE//$'\n'/'%0A'}" - GIT_TAG_MESSAGE="${GIT_TAG_MESSAGE//$'\r'/'%0D'}" - echo "::set-output name=git_tag_message::${GIT_TAG_MESSAGE}" - - - name: Push tags to GitHub (if any) - if: steps.tag-and-publish.outputs.tagged == 1 - run: git push --tags - - - name: Extract release notes - if: steps.tag-and-publish.outputs.tagged == 1 - id: extract-release-notes - uses: ffurrer2/extract-release-notes@c24866884b7a0d2fd2095be2e406b6f260479da8 - - - name: Create release - if: steps.tag-and-publish.outputs.tagged == 1 - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ env.GIT_LATEST_TAG }} - release_name: ${{ env.GIT_LATEST_TAG }} - body: | - ## What's changed? - - ${{ steps.extract-release-notes.outputs.release_notes }} - - **Crate Link**: ${{ steps.tag-and-publish.outputs.git_tag_message }} - - **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ env.GIT_PREVIOUS_TAG }}...${{ env.GIT_LATEST_TAG }} + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: MarcoIeni/release-plz-action@v0.5 + env: + # https://marcoieni.github.io/release-plz/github-action.html#triggering-further-workflow-runs + GITHUB_TOKEN: ${{ secrets.CUSTOM_GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.gitignore b/.gitignore index 96ef6c0b..990b45ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ +# Rust artifacts /target Cargo.lock + +# IDEs +.idea +.vscode + +# macOS +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 678d6f9c..48cd1cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Added the `methods::to_json()` helper method for visualizing the serialization of the RPC methods. -- Extracted all the RPC methods into their own modules instead of all being defined in the same `methods.rs` file. -- Moved auth specific logic behind a feature flag. +## [0.6.0](https://github.com/near/near-jsonrpc-client-rs/compare/v0.5.1...v0.6.0) - 2023-06-02 + +### Other +- [**breaking**] Upgrade near primitive crates version to 0.17.0 ([#126](https://github.com/near/near-jsonrpc-client-rs/pull/126)) + +## [0.5.1] - 2023-03-22 + +- Updated `borsh` to `0.10.2`. + +## [0.5.0] - 2023-02-24 + +### Added + +- `ApiKey::new` now accepts byte arrays and byte slices. +- `Authorization::bearer` method for token-authenticated requests. +- `ApiKey::as_bytes` returns a byte slice of the key without utf-8 validation. + +### Changed + +- Updated nearcore dependencies to `0.16.0`, which now requires a MSRV of `1.67.1`. +- `ApiKey::new` no longer requres the input of a valid UUID. +- `Debug` on `ApiKey` doesn't reveal the key anymore. +- The `auth` module is no longer feature gated. + +### Breaking + +- Removed the `auth::IntoApiKey` trait, any thing you can get a byte slice from is now a valid `ApiKey` input. +- Replaced the `ApiKey::as_str` method with `ApiKey::to_str`, now returning a `Result`. +- Replaced the `InvalidApiKey` error with `InvalidHeaderValue` re-exported from `http`. +- Removed `Display` on `ApiKey`. + +## [0.4.1] - 2022-11-11 + +- Fixed an issue where an `&RpcMethod`'s response was being parsed differently from an `RpcMethod`. + +## [0.4.0] - 2022-10-04 + +- Updated nearcore dependencies, which now requires a MSRV of `1.64.0`. , +- Updated other dependencies, with some general improvements. +- Added `rustls-tls` feature flag to enable `rustls` as an alternative to `native-tls`. +- Switched to using `log::debug!` instead of `log::info!` for debug logging. - Fixed `gas_price` RPC method serialization. - Fixed `query` method error deserialization. +- Reworked the `JsonRpcError`::`handler_error` method. +- Moved auth specific logic behind a feature flag. +- Added the `methods::to_json()` helper method for visualizing the serialization of the RPC methods. + +## [0.4.0-beta.0] - 2022-05-31 + +
+ + + Superseded by + 0.4.0 + + + + +> - Updated nearcore dependencies, fixing a previous breaking change. +> - Fixed `gas_price` RPC method serialization. +> - Fixed `query` method error deserialization. +> - Reworked the `JsonRpcError`::`handler_error` method. +> - Moved auth specific logic behind a feature flag. +> - Added the `methods::to_json()` helper method for visualizing the serialization of the RPC methods. + +
## [0.3.0] - 2022-02-09 @@ -36,7 +97,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > Release Page: -[unreleased]: https://github.com/near/near-jsonrpc-client-rs/compare/v0.3.0...HEAD +[unreleased]: https://github.com/near/near-jsonrpc-client-rs/compare/v0.4.1...HEAD +[0.4.1]: https://github.com/near/near-jsonrpc-client-rs/compare/v0.4.0...v0.4.1 +[0.4.0]: https://github.com/near/near-jsonrpc-client-rs/compare/v0.3.0...v0.4.0 +[0.4.0-beta.0]: https://github.com/near/near-jsonrpc-client-rs/compare/v0.3.0...v0.4.0-beta.0 [0.3.0]: https://github.com/near/near-jsonrpc-client-rs/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/near/near-jsonrpc-client-rs/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/near/near-jsonrpc-client-rs/releases/tag/v0.1.0 diff --git a/Cargo.toml b/Cargo.toml index 70d79a72..ddf33a73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "near-jsonrpc-client" -version = "0.0.0" # managed by cargo-workspaces, see below +version = "0.6.0" authors = ["Near Inc "] edition = "2021" license = "MIT OR Apache-2.0" @@ -8,39 +8,36 @@ repository = "https://github.com/near/near-jsonrpc-client-rs" description = "Lower-level API for interfacing with the NEAR Protocol via JSONRPC" categories = ["asynchronous", "api-bindings", "network-programming"] keywords = ["near", "api", "jsonrpc", "rpc", "async"] -rust-version = "1.56.0" - -# cargo-workspaces -[workspace.metadata.workspaces] -version = "0.3.0" +rust-version = "1.67.1" [dependencies] -uuid = { version = "0.8", features = ["v4"], optional = true } -borsh = "0.9" -serde = "1.0.127" -reqwest = { version = "0.11.4", features = ["json"] } -thiserror = "1.0.28" -serde_json = "1.0.66" +log = "0.4.17" +borsh = "1.3.0" +serde = "1.0.145" +reqwest = { version = "0.11.12", features = ["json"], default-features = false } +thiserror = "1.0.37" +serde_json = "1.0.85" lazy_static = "1.4.0" -near-crypto = "0.12.0" -near-primitives = "0.12.0" -near-chain-configs = "0.12.0" -near-jsonrpc-primitives = "0.12.0" +near-crypto = "0.19.0" +near-primitives = "0.19.0" +near-chain-configs = "0.19.0" +near-jsonrpc-primitives = "0.19.0" [dev-dependencies] -tokio = { version = "1.1", features = ["rt", "macros"] } +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +env_logger = "0.10.0" [features] -default = ["auth"] +default = ["native-tls"] any = [] -auth = ["uuid"] sandbox = [] adversarial = [] +native-tls = ["reqwest/native-tls"] +rustls-tls = ["reqwest/rustls-tls"] [[example]] name = "auth" -required-features = ["auth"] [package.metadata.docs.rs] -features = ["any", "auth", "sandbox"] +features = ["any", "sandbox"] diff --git a/README.md b/README.md index 9c4d302c..73f84540 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ Lower-level API for interfacing with the NEAR Protocol via JSONRPC. -[![crates.io](https://img.shields.io/crates/v/near-jsonrpc-client?label=latest)](https://crates.io/crates/near-jsonrpc-client) +[![Crates.io](https://img.shields.io/crates/v/near-jsonrpc-client?label=latest)](https://crates.io/crates/near-jsonrpc-client) [![Documentation](https://docs.rs/near-jsonrpc-client/badge.svg)](https://docs.rs/near-jsonrpc-client) -![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/near-jsonrpc-client.svg) -[![Dependency Status](https://deps.rs/crate/near-jsonrpc-client/0.3.0/status.svg)](https://deps.rs/crate/near-jsonrpc-client/0.3.0) +[![MIT or Apache 2.0 Licensed](https://img.shields.io/crates/l/near-jsonrpc-client.svg)](#license) +[![Dependency Status](https://deps.rs/crate/near-jsonrpc-client/0.5.1/status.svg)](https://deps.rs/crate/near-jsonrpc-client/0.5.1) ## Usage @@ -49,7 +49,7 @@ use serde::Deserialize; use serde_json::json; use near_jsonrpc_client::{methods, JsonRpcClient}; -use near_primitives::serialize::u128_dec_format; +use near_primitives::serialize::dec_format; use near_primitives::types::*; #[derive(Debug, Deserialize)] @@ -58,11 +58,11 @@ struct PartialGenesisConfig { chain_id: String, genesis_height: BlockHeight, epoch_length: BlockHeightDelta, - #[serde(with = "u128_dec_format")] + #[serde(with = "dec_format")] min_gas_price: Balance, - #[serde(with = "u128_dec_format")] + #[serde(with = "dec_format")] max_gas_price: Balance, - #[serde(with = "u128_dec_format")] + #[serde(with = "dec_format")] total_supply: Balance, validators: Vec, } @@ -81,6 +81,15 @@ let partial_genesis = mainnet_client.call(genesis_config_request).await?; println!("{:#?}", partial_genesis); ``` +By default, `near-jsonrpc-client` uses `native-tls`. On Linux, this introduces a dependency on the system `openssl` library. In some situations, for example when cross-compiling, it can be problematic to depend on non-Rust libraries. + +If you wish to switch to an all-Rust TLS implementation, you may do so using the `rustls-tls` feature flag. Note that the `native-tls` feature is enabled by default. Therefore, to disable it and use `rustls-tls` instead, you must also use `default-features = false`. The default `auth` feature must then be declared explicitly. + +```toml +# in Cargo.toml +near-jsonrpc-client = { ..., default-features = false, features = ["auth","rustls-tls"] } +``` + ## Releasing Versioning and releasing of this crate is automated and managed by [custom fork](https://github.com/miraclx/cargo-workspaces/tree/grouping-versioning-and-exclusion) of [`cargo-workspaces`](https://github.com/pksunkara/cargo-workspaces). To publish a new version of this crate, you can do so by bumping the `version` under the `[workspace.metadata.workspaces]` section in the [package manifest](https://github.com/near/near-jsonrpc-client-rs/blob/master/Cargo.toml) and submit a PR. diff --git a/examples/access_keys.rs b/examples/access_keys.rs index 807e8ded..48edfe20 100644 --- a/examples/access_keys.rs +++ b/examples/access_keys.rs @@ -17,6 +17,8 @@ fn indent(indentation: usize, s: String) -> String { #[tokio::main] async fn main() -> Result<(), Box> { + env_logger::init(); + let client = utils::select_network()?; let account_id = utils::input("Enter the Account ID whose keys we're listing: ")?.parse()?; diff --git a/examples/auth.rs b/examples/auth.rs index 0abcf4bf..6203bea4 100644 --- a/examples/auth.rs +++ b/examples/auth.rs @@ -5,47 +5,64 @@ use near_jsonrpc_client::errors::{ use near_jsonrpc_client::{auth, methods, JsonRpcClient}; use near_primitives::types::{BlockReference, Finality}; +mod utils; + async fn unauthorized() -> Result<(), Box> { - let client = JsonRpcClient::connect("https://rpc.testnet.near.org"); + let client = JsonRpcClient::connect("https://near-mainnet.api.pagoda.co/rpc/v1/"); let request = methods::block::RpcBlockRequest { block_reference: BlockReference::Finality(Finality::Final), }; - let response = client.call(request).await; - - assert!( - matches!( - response, - Err(ServerError(ResponseStatusError(Unauthorized))) - ), - "got {:?}", - response - ); + match client.call(request).await { + Ok(_) => panic!("The unauthorized request succeeded unexpectedly."), + Err(ServerError(ResponseStatusError(Unauthorized))) => { + eprintln!("\x1b[33mThe unauthorized request failed as expected.\x1b[0m"); + } + Err(error) => { + eprintln!("\x1b[31mThe unauthorized request failed with an unexpected error.\x1b[0m"); + eprintln!("Error: {:#?}", error); + } + } Ok(()) } -async fn authorized() -> Result<(), Box> { - let client = JsonRpcClient::connect("https://rpc.testnet.near.org") - .header(auth::ApiKey::new("399ba741-e939-4ffa-8c3c-306ec36fa8de")?); +async fn authorized(api_key: &str) -> Result<(), Box> { + let client = JsonRpcClient::connect("https://near-mainnet.api.pagoda.co/rpc/v1/") + .header(auth::ApiKey::new(api_key)?); let request = methods::block::RpcBlockRequest { block_reference: BlockReference::Finality(Finality::Final), }; - let response = client.call(request).await?; - - println!("{:#?}", response); + match client.call(request).await { + Ok(block) => println!("{:#?}", block), + Err(error) => { + eprintln!( + "\x1b[31mThe authorized request failed unexpectedly, is the API key valid?\x1b[0m" + ); + match error { + ServerError(ResponseStatusError(Unauthorized)) => { + println!("Unauthorized: {}", error) + } + _ => println!("Unexpected error: {}", error), + } + } + } Ok(()) } #[tokio::main] async fn main() -> Result<(), Box> { - unauthorized().await?; + env_logger::init(); - authorized().await?; + let input = utils::input("Enter an API Key: ")?; + + authorized(&input).await?; + + unauthorized().await?; Ok(()) } diff --git a/examples/contract_change_method.rs b/examples/contract_change_method.rs index 0b0fda4e..504a4e21 100644 --- a/examples/contract_change_method.rs +++ b/examples/contract_change_method.rs @@ -11,6 +11,8 @@ mod utils; #[tokio::main] async fn main() -> Result<(), Box> { + env_logger::init(); + let client = JsonRpcClient::connect("https://rpc.testnet.near.org"); let signer_account_id = utils::input("Enter the signer Account ID: ")?.parse()?; @@ -42,7 +44,7 @@ async fn main() -> Result<(), Box> { nonce: current_nonce + 1, receiver_id: "nosedive.testnet".parse()?, block_hash: access_key_query_response.block_hash, - actions: vec![Action::FunctionCall(FunctionCallAction { + actions: vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "rate".to_string(), args: json!({ "account_id": other_account, @@ -52,7 +54,7 @@ async fn main() -> Result<(), Box> { .into_bytes(), gas: 100_000_000_000_000, // 100 TeraGas deposit: 0, - })], + }))], }; let request = methods::broadcast_tx_async::RpcBroadcastTxAsyncRequest { @@ -66,8 +68,8 @@ async fn main() -> Result<(), Box> { let response = client .call(methods::tx::RpcTransactionStatusRequest { transaction_info: TransactionInfo::TransactionId { - hash: tx_hash, - account_id: signer.account_id.clone(), + tx_hash, + sender_account_id: signer.account_id.clone(), }, }) .await; @@ -79,12 +81,12 @@ async fn main() -> Result<(), Box> { } match response { - Err(err) => match err.handler_error()? { - methods::tx::RpcTransactionError::UnknownTransaction { .. } => { + Err(err) => match err.handler_error() { + Some(methods::tx::RpcTransactionError::UnknownTransaction { .. }) => { time::sleep(time::Duration::from_secs(2)).await; continue; } - err => Err(err)?, + _ => Err(err)?, }, Ok(response) => { println!("response gotten after: {}s", delta); diff --git a/examples/contract_change_method_commit.rs b/examples/contract_change_method_commit.rs index e339ce72..db074eb2 100644 --- a/examples/contract_change_method_commit.rs +++ b/examples/contract_change_method_commit.rs @@ -9,6 +9,8 @@ mod utils; #[tokio::main] async fn main() -> Result<(), Box> { + env_logger::init(); + let client = JsonRpcClient::connect("https://rpc.testnet.near.org"); let signer_account_id = utils::input("Enter the signer Account ID: ")?.parse()?; @@ -40,7 +42,7 @@ async fn main() -> Result<(), Box> { nonce: current_nonce + 1, receiver_id: "nosedive.testnet".parse()?, block_hash: access_key_query_response.block_hash, - actions: vec![Action::FunctionCall(FunctionCallAction { + actions: vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: "rate".to_string(), args: json!({ "account_id": other_account, @@ -50,7 +52,7 @@ async fn main() -> Result<(), Box> { .into_bytes(), gas: 100_000_000_000_000, // 100 TeraGas deposit: 0, - })], + }))], }; let request = methods::broadcast_tx_commit::RpcBroadcastTxCommitRequest { diff --git a/examples/contract_view_code.rs b/examples/contract_view_code.rs index af702176..6b8b45aa 100644 --- a/examples/contract_view_code.rs +++ b/examples/contract_view_code.rs @@ -7,6 +7,8 @@ mod utils; #[tokio::main] async fn main() -> Result<(), Box> { + env_logger::init(); + let client = utils::select_network()?; let contract_id: AccountId = diff --git a/examples/contract_view_method.rs b/examples/contract_view_method.rs index 06a714e2..70159815 100644 --- a/examples/contract_view_method.rs +++ b/examples/contract_view_method.rs @@ -17,6 +17,8 @@ pub struct AccountStatus { #[tokio::main] async fn main() -> Result<(), Box> { + env_logger::init(); + let client = JsonRpcClient::connect("https://rpc.testnet.near.org"); let account_id = utils::input("Enter the account to view: ")?; diff --git a/examples/contract_view_state.rs b/examples/contract_view_state.rs new file mode 100644 index 00000000..0c4418b1 --- /dev/null +++ b/examples/contract_view_state.rs @@ -0,0 +1,33 @@ +use near_jsonrpc_client::methods; +use near_jsonrpc_primitives::types::query::QueryResponseKind; +use near_primitives::types::{AccountId, BlockReference, Finality}; +use near_primitives::views::QueryRequest; + +mod utils; + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + let client = utils::select_network()?; + + let contract_id: AccountId = + utils::input("Enter the contract whose state you want to inspect: ")?.parse()?; + + let request = methods::query::RpcQueryRequest { + block_reference: BlockReference::Finality(Finality::Final), + request: QueryRequest::ViewState { + account_id: contract_id.clone(), + prefix: near_primitives::types::StoreKey::from(Vec::new()), + include_proof: false, + }, + }; + + let response = client.call(request).await?; + + if let QueryResponseKind::ViewState(result) = response.kind { + println!("{:#?}", result); + } + + Ok(()) +} diff --git a/examples/create_account.rs b/examples/create_account.rs new file mode 100644 index 00000000..9995e182 --- /dev/null +++ b/examples/create_account.rs @@ -0,0 +1,256 @@ +//! Creates an account on the network. +//! +//! Creates either; +//! - a top-level mainnet / testnet account +//! - or a sub-account for any account on the network. +//! +//! top-level account example: `miraclx.near` creates `foobar.near` +//! sub-account example: `miraclx.near` creates `test.miraclx.near` +//! +//! This script is interactive. + +use near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError; +use near_jsonrpc_client::{methods, JsonRpcClient}; +use near_jsonrpc_primitives::types::query::QueryResponseKind; +use near_jsonrpc_primitives::types::transactions::TransactionInfo; +use near_primitives::hash::CryptoHash; +use near_primitives::transaction::{ + Action, AddKeyAction, CreateAccountAction, FunctionCallAction, Transaction, TransferAction, +}; +use near_primitives::types::{AccountId, BlockReference}; + +use serde_json::json; +use tokio::time; + +mod utils; + +async fn account_exists( + client: &JsonRpcClient, + account_id: &AccountId, +) -> Result> { + let access_key_query_response = client + .call(methods::query::RpcQueryRequest { + block_reference: BlockReference::latest(), + request: near_primitives::views::QueryRequest::ViewAccount { + account_id: account_id.clone(), + }, + }) + .await; + + match access_key_query_response { + Ok(_) => Ok(true), + Err(near_jsonrpc_client::errors::JsonRpcError::ServerError( + near_jsonrpc_client::errors::JsonRpcServerError::HandlerError( + near_jsonrpc_primitives::types::query::RpcQueryError::UnknownAccount { .. }, + ), + )) => Ok(false), + Err(res) => Err(res)?, + } +} + +async fn get_current_nonce( + client: &JsonRpcClient, + account_id: &AccountId, + public_key: &near_crypto::PublicKey, +) -> Result, Box> { + let query_response = client + .call(methods::query::RpcQueryRequest { + block_reference: BlockReference::latest(), + request: near_primitives::views::QueryRequest::ViewAccessKey { + account_id: account_id.clone(), + public_key: public_key.clone(), + }, + }) + .await; + + match query_response { + Ok(access_key_query_response) => match access_key_query_response.kind { + QueryResponseKind::AccessKey(access_key) => Ok(Some(( + access_key_query_response.block_hash, + access_key.nonce, + ))), + _ => Err("failed to extract current nonce")?, + }, + Err(near_jsonrpc_client::errors::JsonRpcError::ServerError( + near_jsonrpc_client::errors::JsonRpcServerError::HandlerError( + near_jsonrpc_primitives::types::query::RpcQueryError::UnknownAccessKey { .. }, + ), + )) => Ok(None), + Err(res) => Err(res)?, + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + let client = utils::select_network()?; + + let signer_account_id = loop { + let signer_account_id = utils::input("Enter the creators Account ID: ")?.parse()?; + if account_exists(&client, &signer_account_id).await? { + break signer_account_id; + } + println!("(i) This account doesn't exist, please reenter!"); + }; + + let (signer, latest_hash, current_nonce) = loop { + let signer_secret_key = utils::input("Enter the creators's private key: ")?.parse()?; + + let signer = near_crypto::InMemorySigner::from_secret_key( + signer_account_id.clone(), + signer_secret_key, + ); + + if let Some((latest_hash, current_nonce)) = + get_current_nonce(&client, &signer.account_id, &signer.public_key).await? + { + break (signer, latest_hash, current_nonce); + } + println!("(i) Invalid access key, please reenter!"); + }; + + let new_account_id = loop { + let new_account_id = utils::input("What's the new Account ID: ")?.parse()?; + if !account_exists(&client, &new_account_id).await? { + break new_account_id; + } + println!("(i) This account already exists, please reenter!"); + }; + + let initial_deposit = loop { + let deposit: f64 = + utils::input("How much do you want to fund this account with (in Ⓝ units)? ")? + .parse()?; + if deposit >= 0.0 { + break ((deposit * 1_000_000.0) as u128) * 1_000_000_000_000_000_000 as u128; + } + println!("(i) Enter a non-zero deposit value!"); + }; + + let is_sub_account = new_account_id.is_sub_account_of(&signer.account_id); + let new_key_pair = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519); + + let transaction = if is_sub_account { + Transaction { + signer_id: signer.account_id.clone(), + public_key: signer.public_key.clone(), + nonce: current_nonce + 1, + receiver_id: new_account_id.clone(), + block_hash: latest_hash, + actions: vec![ + Action::CreateAccount(CreateAccountAction {}), + Action::AddKey(Box::new(AddKeyAction { + access_key: near_primitives::account::AccessKey { + nonce: 0, + permission: near_primitives::account::AccessKeyPermission::FullAccess, + }, + public_key: new_key_pair.public_key(), + })), + Action::Transfer(TransferAction { + deposit: initial_deposit, + }), + ], + } + } else { + let contract_id = if client.server_addr().ends_with("testnet.near.org") { + "testnet".parse()? + } else if client.server_addr().ends_with("mainnet.near.org") { + "near".parse()? + } else { + Err("can only create non-sub accounts for mainnet / testnet\nconsider creating a sub-account instead")? + }; + Transaction { + signer_id: signer.account_id.clone(), + public_key: signer.public_key.clone(), + nonce: current_nonce + 1, + receiver_id: contract_id, + block_hash: latest_hash, + actions: vec![Action::FunctionCall(Box::new(FunctionCallAction { + method_name: "create_account".to_string(), + args: json!({ + "new_account_id": new_account_id, + "new_public_key": new_key_pair.public_key(), + }) + .to_string() + .into_bytes(), + gas: 300_000_000_000_000, + deposit: initial_deposit, + }))], + } + }; + + println!("============================================================="); + println!("New Account ID: {}", new_account_id); + println!(" Secret Key: {}", new_key_pair); + println!(" Public Key: {}", new_key_pair.public_key()); + println!(" Deposit: {}", initial_deposit); + println!("-------------------------------------------------------------"); + + let request = methods::broadcast_tx_async::RpcBroadcastTxAsyncRequest { + signed_transaction: transaction.sign(&signer), + }; + + let sent_at = time::Instant::now(); + + let tx_hash = client.call(request).await?; + + println!(" Tx Hash: {}", tx_hash); + println!("============================================================="); + + loop { + let response = client + .call(methods::tx::RpcTransactionStatusRequest { + transaction_info: TransactionInfo::TransactionId { + tx_hash, + sender_account_id: signer.account_id.clone(), + }, + }) + .await; + let received_at = time::Instant::now(); + let delta = (received_at - sent_at).as_secs(); + + if delta > 60 { + Err("time limit exceeded for the transaction to be recognized")?; + } + + match response { + Ok( + ref outcome @ near_primitives::views::FinalExecutionOutcomeView { + status: near_primitives::views::FinalExecutionStatus::SuccessValue(ref s), + .. + }, + ) => { + // outcome.status != SuccessValue(`false`) + if s == b"false" { + println!("(i) Account successfully created after {}s", delta); + } else { + println!("{:#?}", outcome); + println!("(!) Creating the account failed, check above for full logs"); + } + break; + } + Ok(near_primitives::views::FinalExecutionOutcomeView { + status: near_primitives::views::FinalExecutionStatus::Failure(err), + .. + }) => { + println!("{:#?}", err); + println!("(!) Creating the account failed, check above for full logs"); + break; + } + Err(err) => match err.handler_error() { + Some( + RpcTransactionError::TimeoutError + | methods::tx::RpcTransactionError::UnknownTransaction { .. }, + ) => { + time::sleep(time::Duration::from_secs(2)).await; + continue; + } + _ => Err(err)?, + }, + _ => {} + } + } + + Ok(()) +} diff --git a/examples/query_block.rs b/examples/query_block.rs new file mode 100644 index 00000000..9032518d --- /dev/null +++ b/examples/query_block.rs @@ -0,0 +1,91 @@ +use near_jsonrpc_client::methods; + +mod utils; + +pub fn specify_block_reference() -> std::io::Result { + println!("=========[Block Reference]========="); + let block_reference = utils::select( + || { + println!(" [1] final \x1b[38;5;244m(alias: f, fin)\x1b[0m"); + println!(" [2] optimistic \x1b[38;5;244m(alias: o, opt)\x1b[0m"); + println!(" [3] block hash \x1b[38;5;244m(alias: s, hash)\x1b[0m"); + println!(" [4] block height \x1b[38;5;244m(alias: h, height)\x1b[0m"); + }, + "\x1b[33m(enter a selection)\x1b[0m> ", + |selection| match (selection, selection.parse()) { + ("f" | "fin" | "final", _) | (_, Ok(1)) => { + Some(near_primitives::types::BlockReference::Finality( + near_primitives::types::Finality::Final, + )) + } + ("o" | "opt" | "optimistic", _) | (_, Ok(2)) => { + Some(near_primitives::types::BlockReference::Finality( + near_primitives::types::Finality::None, + )) + } + ("s" | "hash" | "block hash", _) | (_, Ok(3)) => loop { + match utils::input("What block hash should we query? ") + .unwrap() + .parse() + { + Ok(block_hash) => { + break Some(near_primitives::types::BlockReference::BlockId( + near_primitives::types::BlockId::Hash(block_hash), + )) + } + _ => println!("(i) Invalid block hash, please reenter!"), + } + }, + ("h" | "height" | "block height", _) | (_, Ok(4)) => loop { + match utils::input("What block height should we query? ") + .unwrap() + .parse() + { + Ok(block_height) => { + break Some(near_primitives::types::BlockReference::BlockId( + near_primitives::types::BlockId::Height(block_height), + )) + } + _ => println!("(i) Invalid block height, please reenter!"), + } + }, + _ => None, + }, + )?; + println!("==================================="); + + Ok(block_reference) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + let client = utils::select_network()?; + + // tolerate only 3 retries + for _ in 1..=3 { + let block_reference = specify_block_reference()?; + + match client + .call(methods::block::RpcBlockRequest { block_reference }) + .await + { + Ok(block_details) => println!("{:#?}", block_details), + Err(err) => match err.handler_error() { + Some(methods::block::RpcBlockError::UnknownBlock { .. }) => { + println!("(i) Unknown block!"); + continue; + } + Some(err) => { + println!("(i) An error occurred `{:#?}`", err); + continue; + } + _ => println!("(i) A non-handler error ocurred `{:#?}`", err), + }, + }; + break; + } + + Ok(()) +} diff --git a/examples/query_final_block.rs b/examples/query_final_block.rs deleted file mode 100644 index 2b08f487..00000000 --- a/examples/query_final_block.rs +++ /dev/null @@ -1,19 +0,0 @@ -use near_jsonrpc_client::methods; -use near_primitives::types::{BlockReference, Finality}; - -mod utils; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let client = utils::select_network()?; - - let request = methods::block::RpcBlockRequest { - block_reference: BlockReference::Finality(Finality::Final), - }; - - let response = client.call(request).await?; - - println!("{:#?}", response); - - Ok(()) -} diff --git a/examples/query_tx.rs b/examples/query_tx.rs new file mode 100644 index 00000000..5236f7b0 --- /dev/null +++ b/examples/query_tx.rs @@ -0,0 +1,118 @@ +use near_jsonrpc_client::methods; + +mod utils; + +pub fn specify_block_reference() -> std::io::Result { + println!("=========[Block Reference]========="); + let block_reference = utils::select( + || { + println!(" [1] final \x1b[38;5;244m(alias: f, fin)\x1b[0m"); + println!(" [2] optimistic \x1b[38;5;244m(alias: o, opt)\x1b[0m"); + println!(" [3] block hash \x1b[38;5;244m(alias: s, hash)\x1b[0m"); + println!(" [4] block height \x1b[38;5;244m(alias: h, height)\x1b[0m"); + }, + "\x1b[33m(enter a selection)\x1b[0m> ", + |selection| match (selection, selection.parse()) { + ("f" | "fin" | "final", _) | (_, Ok(1)) => { + Some(near_primitives::types::BlockReference::Finality( + near_primitives::types::Finality::Final, + )) + } + ("o" | "opt" | "optimistic", _) | (_, Ok(2)) => { + Some(near_primitives::types::BlockReference::Finality( + near_primitives::types::Finality::None, + )) + } + ("s" | "hash" | "block hash", _) | (_, Ok(3)) => loop { + match utils::input("What block hash should we query? ") + .unwrap() + .parse() + { + Ok(block_hash) => { + break Some(near_primitives::types::BlockReference::BlockId( + near_primitives::types::BlockId::Hash(block_hash), + )) + } + _ => println!("(i) Invalid block hash, please reenter!"), + } + }, + ("h" | "height" | "block height", _) | (_, Ok(4)) => loop { + match utils::input("What block height should we query? ") + .unwrap() + .parse() + { + Ok(block_height) => { + break Some(near_primitives::types::BlockReference::BlockId( + near_primitives::types::BlockId::Height(block_height), + )) + } + _ => println!("(i) Invalid block height, please reenter!"), + } + }, + _ => None, + }, + )?; + println!("==================================="); + + Ok(block_reference) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + let client = utils::select_network()?; + + // tolerate only 3 retries for a non-failing transaction hash + 'root: for _ in 1..=3 { + let tx_hash = 'tx_hash: loop { + // tolerate only 3 retries for a valid transaction hash + for _ in 1..=3 { + if let Ok(tx_hash) = + utils::input("What transaction hash should we query? ")?.parse() + { + break 'tx_hash tx_hash; + } + println!("(i) Invalid transaction hash!"); + } + + break 'root; + }; + + let account_id = 'account_id: loop { + // tolerate only 3 retries for a valid Account ID + for _ in 1..=3 { + if let Ok(account_id) = + utils::input("What account signed this transaction? ")?.parse() + { + break 'account_id account_id; + } + println!("(i) Invalid Account ID!"); + } + + break 'root; + }; + + match client + .call(methods::tx::RpcTransactionStatusRequest { + transaction_info: methods::tx::TransactionInfo::TransactionId { + tx_hash, + sender_account_id: account_id, + }, + }) + .await + { + Ok(block_details) => println!("{:#?}", block_details), + Err(err) => match err.handler_error() { + Some(err) => { + println!("(i) An error occurred `{:#?}`", err); + continue; + } + _ => println!("(i) A non-handler error ocurred `{:#?}`", err), + }, + }; + break; + } + + Ok(()) +} diff --git a/examples/utils.rs b/examples/utils.rs index 74929d2d..f4f30c89 100644 --- a/examples/utils.rs +++ b/examples/utils.rs @@ -11,7 +11,10 @@ pub fn input(query: &str) -> io::Result { Ok(input.trim().to_owned()) } -fn select(print_msg: fn(), query: &str, chk: fn(&str) -> Option) -> io::Result { +pub fn select(print_msg: fn(), query: &str, chk: F) -> io::Result +where + F: Fn(&str) -> Option, +{ loop { print_msg(); for _ in 1..=5 { @@ -36,7 +39,7 @@ pub fn select_network() -> io::Result { |selection| match (selection, selection.parse()) { ("m" | "main" | "mainnet", _) | (_, Ok(1)) => Some("mainnet"), ("t" | "test" | "testnet", _) | (_, Ok(2)) => Some("testnet"), - ("c" | "custom", _) | (_, Ok(2)) => Some("custom"), + ("c" | "custom", _) | (_, Ok(3)) => Some("custom"), _ => None, }, )?; diff --git a/examples/view_account.rs b/examples/view_account.rs index 760ecaf1..9832fb41 100644 --- a/examples/view_account.rs +++ b/examples/view_account.rs @@ -7,6 +7,8 @@ mod utils; #[tokio::main] async fn main() -> Result<(), Box> { + env_logger::init(); + let client = utils::select_network()?; let account_id: AccountId = utils::input("Enter an Account ID to lookup: ")?.parse()?; diff --git a/src/auth.rs b/src/auth.rs index fd676360..37d8c9bf 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,6 +1,63 @@ -use std::fmt; - -use reqwest::header::HeaderValue; +//! Helpers for client authentication. +//! +//! Some RPC nodes will require authentication before requests can be sent to them. +//! +//! This module provides the [`ApiKey`] and [`Authorization`] types that can be used to authenticate +//! requests. +//! +//! ## Example +//! +//! ### API Key (`x-api-key` Header) +//! +//! ``` +//! use near_jsonrpc_client::{JsonRpcClient, auth, methods}; +//! use near_primitives::types::{BlockReference, Finality}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://rpc.testnet.near.org"); +//! +//! let client = client.header(auth::ApiKey::new("399ba741-e939-4ffa-8c3c-306ec36fa8de")?); +//! +//! let request = methods::block::RpcBlockRequest { +//! block_reference: BlockReference::Finality(Finality::Final), +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!(response, methods::block::RpcBlockResponse { .. })); +//! # Ok(()) +//! # } +//! ``` +//! +//! ### `Authorization` Header +//! +//! ``` +//! use near_jsonrpc_client::{JsonRpcClient, auth, methods}; +//! use near_primitives::types::{BlockReference, Finality}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://rpc.testnet.near.org") +//! .header(auth::Authorization::bearer( +//! "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", +//! )?); +//! +//! let request = methods::block::RpcBlockRequest { +//! block_reference: BlockReference::Finality(Finality::Final), +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!(response, methods::block::RpcBlockResponse { .. })); +//! # Ok(()) +//! # } +//! ``` + +use std::ops::{Index, RangeFrom}; +use std::str; + +use super::header::{HeaderValue, InvalidHeaderValue, ToStrError}; /// NEAR JSON RPC API key. #[derive(Eq, Hash, Clone, Debug, PartialEq)] @@ -9,21 +66,26 @@ pub struct ApiKey(HeaderValue); impl ApiKey { pub const HEADER_NAME: &'static str = "x-api-key"; - /// Creates a new API key from a string. - pub fn new(api_key: K) -> Result { - if let Ok(api_key) = uuid::Uuid::parse_str(api_key.as_ref()) { - if let Ok(api_key) = api_key.to_string().try_into() { - return Ok(ApiKey(api_key)); - } - } - Err(InvalidApiKey { _priv: () }) + /// Creates a new API key. + /// + /// See the [`auth`](self) module documentation for more information. + pub fn new>(api_key: K) -> Result { + HeaderValue::from_bytes(api_key.as_ref()).map(|mut api_key| { + ApiKey({ + api_key.set_sensitive(true); + api_key + }) + }) } - /// Returns the API key as a string slice. - pub fn as_str(&self) -> &str { - self.0 - .to_str() - .expect("fatal: api key should contain only ascii characters") + /// Returns a string slice if the API Key only contains visible ASCII chars. + pub fn to_str(&self) -> Result<&str, ToStrError> { + self.0.to_str() + } + + /// Returns the API key as a byte slice. + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() } } @@ -40,83 +102,99 @@ impl crate::header::HeaderEntry for ApiKey { } } -impl fmt::Display for ApiKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "x-api-key: {}", self.as_str()) - } -} - -/// An error returned when an API key contains invalid characters. -#[derive(Eq, Clone, PartialEq)] -pub struct InvalidApiKey { - _priv: (), +/// HTTP authorization scheme. +#[derive(Eq, Hash, Copy, Clone, Debug, PartialEq)] +#[non_exhaustive] +pub enum AuthorizationScheme { + Bearer, } -impl fmt::Debug for InvalidApiKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("InvalidApiKey") +/// NEAR JSON RPC authorization header. +#[derive(Eq, Hash, Clone, Debug, PartialEq)] +pub struct Authorization(AuthorizationScheme, HeaderValue); + +impl Authorization { + pub const HEADER_NAME: &'static str = "Authorization"; + + /// Creates a new authorization token with the bearer scheme. + /// + /// This does not perform any token-specific validation on the token. + /// + /// See the [`auth`](self) module documentation for more information. + pub fn bearer>(token: T) -> Result { + HeaderValue::from_bytes(&[b"Bearer ", token.as_ref().as_bytes()].concat()).map( + |mut token| { + Authorization(AuthorizationScheme::Bearer, { + token.set_sensitive(true); + token + }) + }, + ) } -} -impl std::error::Error for InvalidApiKey {} -impl fmt::Display for InvalidApiKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Invalid API key") + /// Returns the scheme of the authorization header. + pub fn scheme(&self) -> AuthorizationScheme { + self.0 } -} - -mod private { - pub trait Sealed: AsRef {} -} -/// A marker trait used to identify values that can be made into API keys. -pub trait IntoApiKey: private::Sealed {} - -impl private::Sealed for String {} + /// Returns the token as a string slice. + pub fn as_str(&self) -> &str { + unsafe { str::from_utf8_unchecked(self.as_bytes()) } + } -impl IntoApiKey for String {} + /// Returns the token as a byte slice. + pub fn as_bytes(&self) -> &[u8] { + self.strip_scheme(self.1.as_bytes()) + } -impl private::Sealed for &String {} + fn strip_scheme<'a, T: Index> + ?Sized>(&self, token: &'a T) -> &'a T::Output { + &token[match self.0 { + AuthorizationScheme::Bearer => 7, + }..] + } +} -impl IntoApiKey for &String {} +impl crate::header::HeaderEntry for Authorization { + type HeaderName = &'static str; + type HeaderValue = HeaderValue; -impl private::Sealed for &str {} + fn header_name(&self) -> &Self::HeaderName { + &Self::HEADER_NAME + } -impl IntoApiKey for &str {} + fn header_pair(self) -> (Self::HeaderName, Self::HeaderValue) { + (Self::HEADER_NAME, self.1) + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn api_key() { - ApiKey::new("some-value").expect_err("should not have been a valid API key"); + fn sensitive_debug() { + let api_key = ApiKey::new("this is a very secret secret").expect("valid API key"); - ApiKey::new("0ee1872b-355f-4254-8e2b-1c0b8199ee92") - .expect("should have been a valid API key"); + assert_eq!(format!("{:?}", api_key), "ApiKey(Sensitive)"); - ApiKey::new("0ee1872b355f42548e2b1c0b8199ee92").expect("should have been a valid API key"); + assert_eq!( + api_key.to_str().expect("valid utf8 secret"), + "this is a very secret secret" + ); - ApiKey::new("0ee--1872b355f4254-8e2b1c0b8-199ee92") - .expect_err("should not have been a valid API key"); + assert_eq!(api_key.as_bytes(), b"this is a very secret secret"); } #[test] - fn display() { - let api_key = ApiKey::new("0ee1872b-355f-4254-8e2b-1c0b8199ee92") - .expect("should have been a valid API key"); + fn bearer_token() { + let token = Authorization::bearer("this is a very secret token").expect("valid token"); - assert_eq!( - api_key.to_string(), - "x-api-key: 0ee1872b-355f-4254-8e2b-1c0b8199ee92" - ); + assert_eq!(format!("{:?}", token), "Authorization(Bearer, Sensitive)"); - let api_key = ApiKey::new("0ee1872b355f42548e2b1c0b8199ee92") - .expect("should have been a valid API key"); + assert_eq!(token.scheme(), AuthorizationScheme::Bearer); - assert_eq!( - api_key.to_string(), - "x-api-key: 0ee1872b-355f-4254-8e2b-1c0b8199ee92" - ); + assert_eq!(token.as_str(), "this is a very secret token"); + + assert_eq!(token.as_bytes(), b"this is a very secret token"); } } diff --git a/src/errors.rs b/src/errors.rs index 942b20a5..0e94992c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,4 @@ +//! Error types. use std::io; use thiserror::Error; @@ -5,80 +6,107 @@ use thiserror::Error; use near_jsonrpc_primitives::errors::{RpcError, RpcErrorKind, RpcRequestValidationErrorKind}; use near_jsonrpc_primitives::message::{self, Message}; +/// Potential errors returned while sending a request to the RPC server. #[derive(Debug, Error)] pub enum JsonRpcTransportSendError { + /// Client is unable to serialize the request payload before sending it to the server. #[error("error while serializing payload: [{0}]")] PayloadSerializeError(io::Error), + /// Client is unable to send the request to the server. #[error("error while sending payload: [{0}]")] PayloadSendError(reqwest::Error), } +/// Potential errors returned when the client has an issue parsing the response of a method call. #[derive(Debug, Error)] pub enum JsonRpcTransportHandlerResponseError { + /// Client fails to deserialize the result of a method call. #[error("error while parsing method call result: [{0}]")] ResultParseError(serde_json::Error), + /// Client fails to deserialize the error message returned from a method call. #[error("error while parsing method call error message: [{0}]")] ErrorMessageParseError(serde_json::Error), } +/// Potential errors returned while receiving responses from an RPC server. #[derive(Debug, Error)] pub enum JsonRpcTransportRecvError { + /// Client receives a JSON RPC message body that isn't structured as a response. #[error("unexpected server response: [{0:?}]")] UnexpectedServerResponse(Message), + /// Client is unable to read the response from the RPC server. #[error("error while reading response: [{0}]")] PayloadRecvError(reqwest::Error), + /// The base response structure is malformed e.g. meta properties like RPC version are missing. #[error("error while parsing server response: [{0:?}]")] PayloadParseError(message::Broken), + /// Potential errors returned when the client has an issue parsing the response of a method call. #[error(transparent)] ResponseParseError(JsonRpcTransportHandlerResponseError), } +/// Potential errors returned while sending requests to or receiving responses from the RPC server. #[derive(Debug, Error)] pub enum RpcTransportError { + /// Potential errors returned while sending a request to the RPC server. #[error(transparent)] SendError(JsonRpcTransportSendError), + /// Potential errors returned while receiving a response from an RPC server. #[error(transparent)] RecvError(JsonRpcTransportRecvError), } +/// Unexpected status codes returned by the RPC server. #[derive(Debug, Error)] pub enum JsonRpcServerResponseStatusError { + /// The RPC client is unauthorized. #[error("this client is unauthorized")] Unauthorized, + /// The RPC client exceeds the rate limit by sending too many requests. #[error("this client has exceeded the rate limit")] TooManyRequests, + /// The RPC server returned a non-200 status code. #[error("the server returned a non-OK (200) status code: [{status}]")] Unexpected { status: reqwest::StatusCode }, } +/// Potential errors returned by the RPC server. #[derive(Debug, Error)] pub enum JsonRpcServerError { + /// An invalid RPC method is called or the RPC methdo is unable to parse the provided arguments. #[error("request validation error: [{0:?}]")] RequestValidationError(RpcRequestValidationErrorKind), + /// RPC method call error. #[error("handler error: [{0}]")] HandlerError(E), + /// The RPC server returned an internal server error. #[error("internal error: [{info:?}]")] InternalError { info: Option }, + /// The RPC server returned a response without context i.e. a response the client doesn't expect. #[error("error response lacks context: {0}")] NonContextualError(RpcError), + /// Unexpected status codes returned by the RPC server. #[error(transparent)] ResponseStatusError(JsonRpcServerResponseStatusError), } +/// Potential errors returned by the RPC client. #[derive(Debug, Error)] pub enum JsonRpcError { + /// Potential errors returned while sending requests to or receiving responses from the RPC server. #[error(transparent)] TransportError(RpcTransportError), + /// Potential errors returned by the RPC server. #[error(transparent)] ServerError(JsonRpcServerError), } impl JsonRpcError { - pub fn handler_error(self) -> Result { - match self { - Self::ServerError(JsonRpcServerError::HandlerError(err)) => Ok(err), - err => Err(err), + pub fn handler_error(&self) -> Option<&E> { + if let Self::ServerError(JsonRpcServerError::HandlerError(err)) = self { + return Some(err); } + None } } @@ -111,7 +139,7 @@ impl From for JsonRpcError { None => {} } if let Some(ref raw_err_data) = err.data { - match E::parse_raw_error(raw_err_data.clone()) { + match E::parse_legacy_error(raw_err_data.clone()) { Some(Ok(handler_error)) => { return JsonRpcError::ServerError(JsonRpcServerError::HandlerError( handler_error, diff --git a/src/header.rs b/src/header.rs index 17a68fc2..1526099c 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,13 +1,10 @@ -//! Client Headers. +//! Client headers. //! //! This module includes everything you need to build valid header entries. use std::marker::PhantomData; -pub use reqwest::header::HeaderValue; -use reqwest::header::IntoHeaderName; - -use super::JsonRpcClient; +pub use reqwest::header::{HeaderName, HeaderValue, InvalidHeaderValue, ToStrError}; /// [`HeaderEntry`] attribute identifying those that have been prevalidated. /// @@ -122,74 +119,79 @@ where fn header_pair(self) -> (Self::HeaderName, Self::HeaderValue); } -mod private { - pub trait Sealed {} -} +pub use discriminant::HeaderEntryDiscriminant; +mod discriminant { + use reqwest::header::IntoHeaderName; -/// Trait for defining a [`HeaderEntry`]'s application on a client. -pub trait HeaderEntryDiscriminant: private::Sealed { - type Output; + use super::{super::JsonRpcClient, HeaderEntry, HeaderValue, Postvalidated, Prevalidated}; - fn apply(client: JsonRpcClient, entry: H) -> Self::Output; -} + pub trait Sealed {} -impl private::Sealed for Prevalidated {} -impl HeaderEntryDiscriminant for Prevalidated -where - T: HeaderEntry, - T::HeaderName: IntoHeaderName, -{ - type Output = JsonRpcClient; + /// Trait for defining a [`HeaderEntry`]'s application on a client. + pub trait HeaderEntryDiscriminant: Sealed { + type Output; - fn apply(mut client: JsonRpcClient, entry: T) -> Self::Output { - let (k, v) = entry.header_pair(); - client.headers.insert(k, v); - client + fn apply(client: JsonRpcClient, entry: H) -> Self::Output; } -} -impl private::Sealed for Postvalidated {} -impl HeaderEntryDiscriminant for Postvalidated -where - T: HeaderEntry, - T::HeaderName: IntoHeaderName, - T::HeaderValue: TryInto, -{ - type Output = Result; - - fn apply(mut client: JsonRpcClient, entry: T) -> Self::Output { - let (k, v) = entry.header_pair(); - client.headers.insert(k, v.try_into()?); - Ok(client) + impl Sealed for Prevalidated {} + impl HeaderEntryDiscriminant for Prevalidated + where + T: HeaderEntry, + T::HeaderName: IntoHeaderName, + { + type Output = JsonRpcClient; + + fn apply(mut client: JsonRpcClient, entry: T) -> Self::Output { + let (k, v) = entry.header_pair(); + client.headers.insert(k, v); + client + } } -} - -impl HeaderEntry for (N, HeaderValue) { - type HeaderName = N; - type HeaderValue = HeaderValue; - fn header_name(&self) -> &Self::HeaderName { - &self.0 + impl Sealed for Postvalidated {} + impl HeaderEntryDiscriminant for Postvalidated + where + T: HeaderEntry, + T::HeaderName: IntoHeaderName, + T::HeaderValue: TryInto, + { + type Output = Result; + + fn apply(mut client: JsonRpcClient, entry: T) -> Self::Output { + let (k, v) = entry.header_pair(); + client.headers.insert(k, v.try_into()?); + Ok(client) + } } - fn header_pair(self) -> (Self::HeaderName, Self::HeaderValue) { - self - } -} + impl HeaderEntry for (N, HeaderValue) { + type HeaderName = N; + type HeaderValue = HeaderValue; -impl HeaderEntry> for (N, V) -where - N: IntoHeaderName, - V: TryInto, -{ - type HeaderName = N; - type HeaderValue = V; + fn header_name(&self) -> &Self::HeaderName { + &self.0 + } - fn header_name(&self) -> &Self::HeaderName { - &self.0 + fn header_pair(self) -> (Self::HeaderName, Self::HeaderValue) { + self + } } - fn header_pair(self) -> (Self::HeaderName, Self::HeaderValue) { - self + impl HeaderEntry> for (N, V) + where + N: IntoHeaderName, + V: TryInto, + { + type HeaderName = N; + type HeaderValue = V; + + fn header_name(&self) -> &Self::HeaderName { + &self.0 + } + + fn header_pair(self) -> (Self::HeaderName, Self::HeaderValue) { + self + } } } diff --git a/src/lib.rs b/src/lib.rs index 951b3895..93b9af06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ //! - a `Response` type (e.g [`methods::query::RpcQueryResponse`]) //! - and an `Error` type (e.g [`methods::query::RpcQueryError`]) //! -//! Calling a constructed request on a client returns with the result and error types for that method. +//! Calling a constructed request on a client returns with the response and error types for that method. //! //! ## Examples //! @@ -40,13 +40,13 @@ //! use near_jsonrpc_primitives::types::transactions::TransactionInfo; //! //! # #[tokio::main] -//! # async fn main() -> Result<(), Box> { +//! # async fn main() -> Result<(), Box> { //! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); //! //! let tx_status_request = methods::tx::RpcTransactionStatusRequest { //! transaction_info: TransactionInfo::TransactionId { -//! hash: "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U".parse()?, -//! account_id: "miraclx.near".parse()?, +//! tx_hash: "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U".parse()?, +//! sender_account_id: "miraclx.near".parse()?, //! }, //! }; //! @@ -60,9 +60,6 @@ use std::{fmt, sync::Arc}; use lazy_static::lazy_static; -use near_jsonrpc_primitives::message::{from_slice, Message}; - -#[cfg(feature = "auth")] pub mod auth; pub mod errors; pub mod header; @@ -88,6 +85,8 @@ pub struct JsonRpcClientConnector { impl JsonRpcClientConnector { /// Return a JsonRpcClient that connects to the specified server. pub fn connect(&self, server_addr: U) -> JsonRpcClient { + log::debug!("returned a new JSONRPC client handle"); + JsonRpcClient { inner: Arc::new(JsonRpcInnerClient { server_addr: server_addr.to_string(), @@ -199,6 +198,9 @@ impl JsonRpcClient { )) })?; + log::debug!("request payload: {:#}", request_payload); + log::debug!("request headers: {:#?}", self.headers()); + let request_payload = serde_json::to_vec(&request_payload).map_err(|err| { JsonRpcError::TransportError(RpcTransportError::SendError( JsonRpcTransportSendError::PayloadSerializeError(err.into()), @@ -217,6 +219,7 @@ impl JsonRpcClient { JsonRpcTransportSendError::PayloadSendError(err), )) })?; + log::debug!("response headers: {:#?}", response.headers()); match response.status() { reqwest::StatusCode::OK => {} non_ok_status => { @@ -240,13 +243,22 @@ impl JsonRpcClient { JsonRpcTransportRecvError::PayloadRecvError(err), )) })?; - let response_message = from_slice(&response_payload).map_err(|err| { + let response_payload = serde_json::from_slice::(&response_payload); + + if let Ok(ref response_payload) = response_payload { + log::debug!("response payload: {:#}", response_payload); + } + + let response_message = near_jsonrpc_primitives::message::decoded_to_parsed( + response_payload.and_then(serde_json::from_value), + ) + .map_err(|err| { JsonRpcError::TransportError(RpcTransportError::RecvError( JsonRpcTransportRecvError::PayloadParseError(err), )) })?; - if let Message::Response(response) = response_message { + if let near_jsonrpc_primitives::message::Message::Response(response) = response_message { return M::parse_handler_response(response.result?) .map_err(|err| { JsonRpcError::TransportError(RpcTransportError::RecvError( @@ -277,10 +289,8 @@ impl JsonRpcClient { /// let client = JsonRpcClient::connect("https://rpc.testnet.near.org"); /// let client = client.header(("user-agent", "someclient/0.1.0"))?; // <- returns a result /// - /// # #[cfg(feature = "auth")] /// use near_jsonrpc_client::auth; /// - /// # #[cfg(feature = "auth")] /// let client = client.header( /// auth::ApiKey::new("cadc4c83-5566-4c94-aa36-773605150f44")?, // <- error handling here /// ); // <- returns the client @@ -329,6 +339,7 @@ impl JsonRpcClient { reqwest::header::HeaderValue::from_static("application/json"), ); + log::debug!("initialized a new JSONRPC client connector"); JsonRpcClientConnector { client: reqwest::Client::builder() .default_headers(headers) @@ -397,11 +408,9 @@ impl AsUrl for reqwest::Url {} mod tests { use crate::{methods, JsonRpcClient}; - const RPC_SERVER_ADDR: &'static str = "https://archival-rpc.mainnet.near.org"; - #[tokio::test] async fn chk_status_testnet() { - let client = JsonRpcClient::connect(RPC_SERVER_ADDR); + let client = JsonRpcClient::connect("https://rpc.testnet.near.org"); let status = client.call(methods::status::RpcStatusRequest).await; @@ -414,8 +423,8 @@ mod tests { #[tokio::test] #[cfg(feature = "any")] - async fn any_typed_ok() -> Result<(), Box> { - let client = JsonRpcClient::connect(RPC_SERVER_ADDR); + async fn any_typed_ok() -> Result<(), Box> { + let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); let tx_status = client .call(methods::any::( @@ -431,7 +440,7 @@ mod tests { matches!( tx_status, Ok(methods::tx::RpcTransactionStatusResponse { ref transaction, .. }) - if transaction.signer_id.as_ref() == "miraclx.near" + if transaction.signer_id == "miraclx.near" && transaction.hash == "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U".parse()? ), "expected an Ok(RpcTransactionStatusResponse) with matching signer_id + hash, found [{:?}]", @@ -444,7 +453,7 @@ mod tests { #[tokio::test] #[cfg(feature = "any")] async fn any_typed_err() -> Result<(), Box> { - let client = JsonRpcClient::connect(RPC_SERVER_ADDR); + let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); let tx_error = client .call(methods::any::( @@ -455,16 +464,15 @@ mod tests { ]), )) .await - .expect_err("request must not succeed") - .handler_error(); + .expect_err("request must not succeed"); assert!( matches!( - tx_error, - Ok(methods::tx::RpcTransactionError::UnknownTransaction { + tx_error.handler_error(), + Some(methods::tx::RpcTransactionError::UnknownTransaction { requested_transaction_hash }) - if requested_transaction_hash == "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8D".parse()? + if requested_transaction_hash.to_string() == "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8D" ), "expected an Ok(RpcTransactionError::UnknownTransaction) with matching hash, found [{:?}]", tx_error @@ -476,7 +484,7 @@ mod tests { #[tokio::test] #[cfg(feature = "any")] async fn any_untyped_ok() { - let client = JsonRpcClient::connect(RPC_SERVER_ADDR); + let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); let status = client .call( @@ -506,7 +514,7 @@ mod tests { #[tokio::test] #[cfg(feature = "any")] async fn any_untyped_err() { - let client = JsonRpcClient::connect(RPC_SERVER_ADDR); + let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); let tx_error = client .call( @@ -519,7 +527,8 @@ mod tests { ), ) .await - .expect_err("request must not succeed") + .expect_err("request must not succeed"); + let tx_error = tx_error .handler_error() .expect("expected a handler error from query request"); diff --git a/src/methods/any/mod.rs b/src/methods/any/mod.rs index 0109b8f5..35b8c28d 100644 --- a/src/methods/any/mod.rs +++ b/src/methods/any/mod.rs @@ -1,3 +1,56 @@ +//! For all intents and purposes, the predefined structures in `methods` should suffice, if you find that they +//! don't or you crave extra flexibility, well, you can opt in to use the generic constructor `methods::any()` with the `any` feature flag. +//! +//! In this example, we retrieve only the parts from the genesis config response that we care about. +//! +//! ```toml +//! # in Cargo.toml +//! near-jsonrpc-client = { ..., features = ["any"] } +//! ``` +//! +//! ``` +//! use serde::Deserialize; +//! use serde_json::json; +//! +//! # use near_jsonrpc_client::errors::JsonRpcError; +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::serialize::u128_dec_format; +//! use near_primitives::types::*; +//! +//! #[derive(Debug, Deserialize)] +//! struct PartialGenesisConfig { +//! protocol_version: ProtocolVersion, +//! chain_id: String, +//! genesis_height: BlockHeight, +//! epoch_length: BlockHeightDelta, +//! #[serde(with = "u128_dec_format")] +//! min_gas_price: Balance, +//! #[serde(with = "u128_dec_format")] +//! max_gas_price: Balance, +//! #[serde(with = "u128_dec_format")] +//! total_supply: Balance, +//! validators: Vec, +//! } +//! +//! impl methods::RpcHandlerResponse for PartialGenesisConfig {} +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), JsonRpcError<()>> { +//! let client = JsonRpcClient::connect("https://rpc.mainnet.near.org"); +//! +//! # #[cfg(feature = "any")] { +//! let genesis_config_request = methods::any::>( +//! "EXPERIMENTAL_genesis_config", +//! json!(null), +//! ); +//! +//! let partial_genesis = client.call(genesis_config_request).await?; +//! +//! println!("{:#?}", partial_genesis); +//! # } +//! # Ok(()) +//! # } +//! ``` use super::*; use std::marker::PhantomData; diff --git a/src/methods/block.rs b/src/methods/block.rs index 65920ffb..f00fdd7e 100644 --- a/src/methods/block.rs +++ b/src/methods/block.rs @@ -23,7 +23,7 @@ //! //! ``` //! # use near_jsonrpc_client::methods; -//! # fn main() -> Result<(), Box> { +//! # fn main() -> Result<(), Box> { //! use near_primitives::types::{BlockReference, BlockId}; //! //! let request = methods::block::RpcBlockRequest { @@ -72,7 +72,7 @@ pub type RpcBlockResponse = near_primitives::views::BlockView; impl RpcHandlerResponse for RpcBlockResponse {} impl RpcHandlerError for RpcBlockError { - fn parse_raw_error(value: serde_json::Value) -> Option> { + fn parse(value: serde_json::Value) -> Result { common::parse_unknown_block!(value => Self) } } diff --git a/src/methods/broadcast_tx_async.rs b/src/methods/broadcast_tx_async.rs index 8c3f0fde..e495c27d 100644 --- a/src/methods/broadcast_tx_async.rs +++ b/src/methods/broadcast_tx_async.rs @@ -1,3 +1,58 @@ +//! Sends asynchronous transactions. +//! +//! ## Example +//! +//! Constructs a signed transaction to be sent to an RPC node. It returns the transaction hash if successful. +//! +//! This code sample doesn't make any requests to the RPC node. It only shows how to construct the request. It's been truncated for brevity sake. +//! +//! A full example on how to use `broadcast_tx_async` method can be found at [`contract_change_method`](https://github.com/near/near-jsonrpc-client-rs/blob/master/examples/contract_change_method.rs). +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::types::{AccountId}; +//! use near_primitives::transaction::{Action, FunctionCallAction, Transaction}; +//! use near_crypto::SecretKey; +//! use core::str::FromStr; +//! use serde_json::json; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let signer_account_id = "fido.testnet".parse::()?; +//! let signer_secret_key = SecretKey::from_str("ed25519:12dhevYshfiRqFSu8DSfxA27pTkmGRv6C5qQWTJYTcBEoB7MSTyidghi5NWXzWqrxCKgxVx97bpXPYQxYN5dieU")?; +//! +//! let signer = near_crypto::InMemorySigner::from_secret_key(signer_account_id, signer_secret_key); +//! +//! let other_account = "rpc_docs.testnet".parse::()?; +//! let rating = "4.5".parse::()?; +//! +//! let transaction = Transaction { +//! signer_id: signer.account_id.clone(), +//! public_key: signer.public_key.clone(), +//! nonce: 10223934 + 1, +//! receiver_id: "nosedive.testnet".parse::()?, +//! block_hash: "AUDcb2iNUbsmCsmYGfGuKzyXKimiNcCZjBKTVsbZGnoH".parse()?, +//! actions: vec![Action::FunctionCall(FunctionCallAction { +//! method_name: "rate".to_string(), +//! args: json!({ +//! "account_id": other_account, +//! "rating": rating, +//! }) +//! .to_string() +//! .into_bytes(), +//! gas: 100_000_000_000_000, // 100 TeraGas +//! deposit: 0, +//! })], +//! }; +//! +//! let request = methods::broadcast_tx_async::RpcBroadcastTxAsyncRequest { +//! signed_transaction: transaction.sign(&signer) +//! }; +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_primitives::transaction::SignedTransaction; @@ -10,11 +65,12 @@ pub struct RpcBroadcastTxAsyncRequest { } impl From - for near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest + for near_jsonrpc_primitives::types::transactions::RpcSendTransactionRequest { fn from(this: RpcBroadcastTxAsyncRequest) -> Self { Self { signed_transaction: this.signed_transaction, + wait_until: near_primitives::views::TxExecutionStatus::None, } } } diff --git a/src/methods/broadcast_tx_commit.rs b/src/methods/broadcast_tx_commit.rs index fc264de0..64ffbcab 100644 --- a/src/methods/broadcast_tx_commit.rs +++ b/src/methods/broadcast_tx_commit.rs @@ -1,3 +1,59 @@ +//! Sends blocking transactions. +//! +//! Sends a signed transaction to the RPC and waits until the transaction is fully complete. +//! +//! Constructs a signed transaction to be sent to an RPC node. +//! +//! This code sample doesn't make any requests to the RPC node. It only shows how to construct the request. It's been truncated for brevity. +//! +//! A full example on how to use `broadcast_tx_commit` method can be found at [`contract_change_method`](https://github.com/near/near-jsonrpc-client-rs/blob/master/examples/contract_change_method_commit.rs). +//! +//! ## Example +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_jsonrpc_primitives::types::{query::QueryResponseKind, transactions::TransactionInfo}; +//! use near_primitives::types::{AccountId, BlockReference}; +//! use near_primitives::transaction::{Action, FunctionCallAction, Transaction}; +//! use serde_json::json; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let signer_account_id = "fido.testnet".parse::()?; +//! let signer_secret_key = "ed25519:12dhevYshfiRqFSu8DSfxA27pTkmGRv6C5qQWTJYTcBEoB7MSTyidghi5NWXzWqrxCKgxVx97bpXPYQxYN5dieU".parse()?; +//! +//! let signer = near_crypto::InMemorySigner::from_secret_key(signer_account_id, signer_secret_key); +//! +//! let other_account = "rpc_docs.testnet".parse::()?; +//! let rating = "4.5".parse::()?; +//! +//! let transaction = Transaction { +//! signer_id: signer.account_id.clone(), +//! public_key: signer.public_key.clone(), +//! nonce: 904565 + 1, +//! receiver_id: "nosedive.testnet".parse::()?, +//! block_hash: "AUDcb2iNUbsmCsmYGfGuKzyXKimiNcCZjBKTVsbZGnoH".parse()?, +//! actions: vec![Action::FunctionCall(FunctionCallAction { +//! method_name: "rate".to_string(), +//! args: json!({ +//! "account_id": other_account, +//! "rating": rating, +//! }) +//! .to_string() +//! .into_bytes(), +//! gas: 100_000_000_000_000, // 100 TeraGas +//! deposit: 0, +//! })], +//! }; +//! +//! let request = methods::broadcast_tx_commit::RpcBroadcastTxCommitRequest { +//! signed_transaction: transaction.sign(&signer) +//! }; +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::transactions::RpcTransactionError; @@ -11,11 +67,12 @@ pub struct RpcBroadcastTxCommitRequest { } impl From - for near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest + for near_jsonrpc_primitives::types::transactions::RpcSendTransactionRequest { fn from(this: RpcBroadcastTxCommitRequest) -> Self { Self { signed_transaction: this.signed_transaction, + wait_until: near_primitives::views::TxExecutionStatus::None, } } } diff --git a/src/methods/chunk.rs b/src/methods/chunk.rs index d89024e9..a544881c 100644 --- a/src/methods/chunk.rs +++ b/src/methods/chunk.rs @@ -16,7 +16,7 @@ //! use near_primitives::types::BlockId; //! //! # #[tokio::main] -//! # async fn main() -> Result<(), Box> { +//! # async fn main() -> Result<(), Box> { //! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); //! //! let request = methods::chunk::RpcChunkRequest { @@ -72,7 +72,7 @@ //! use near_jsonrpc_primitives::types::chunks; //! //! # #[tokio::main] -//! # async fn main() -> Result<(), Box> { +//! # async fn main() -> Result<(), Box> { //! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); //! //! let request = methods::chunk::RpcChunkRequest{ @@ -99,7 +99,7 @@ pub type RpcChunkResponse = near_primitives::views::ChunkView; impl RpcHandlerResponse for RpcChunkResponse {} impl RpcHandlerError for RpcChunkError { - fn parse_raw_error(value: serde_json::Value) -> Option> { + fn parse(value: serde_json::Value) -> Result { common::parse_unknown_block!(value => Self) } } diff --git a/src/methods/experimental/changes.rs b/src/methods/experimental/changes.rs index 1a495b7d..46eebb56 100644 --- a/src/methods/experimental/changes.rs +++ b/src/methods/experimental/changes.rs @@ -1,3 +1,156 @@ +//! Returns account changes from transactions in a given account. +//! +//! The `RpcStateChangesInBlockByTypeRequest` struct takes in a `BlockReference` and a `StateChangesRequestView`, and returns an `RpcStateChangesInBlockResponse`. +//! +//! ## Examples +//! +//! The `StateChangesRequestView` enum has a couple of variants that can be used to specify what kind of changes to return. +//! +//! - `AccountChanges` +//! +//! ``` +//! # use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{views::StateChangesRequestView, types::{BlockReference, BlockId}}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_changes::RpcStateChangesInBlockByTypeRequest { +//! block_reference: BlockReference::BlockId(BlockId::Hash("94yBWhN848vHMnKcw5DxgBQWJW6JHRXnXD6FCLJGjxMU".parse()?)), +//! state_changes_request: StateChangesRequestView::AccountChanges { +//! account_ids: vec!["fido.testnet".parse()?, "rpc_docs.testnet".parse()?], +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_changes::RpcStateChangesInBlockResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! - `SingleAccessKeyChanges` +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{views::StateChangesRequestView, types::{BlockReference, BlockId, AccountWithPublicKey}}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_changes::RpcStateChangesInBlockByTypeRequest { +//! block_reference: BlockReference::BlockId(BlockId::Hash("94yBWhN848vHMnKcw5DxgBQWJW6JHRXnXD6FCLJGjxMU".parse()?)), +//! state_changes_request: StateChangesRequestView::SingleAccessKeyChanges { +//! keys: vec![ +//! AccountWithPublicKey { +//! account_id: "fido.testnet".parse()?, +//! public_key: "ed25519:GwRkfEckaADh5tVxe3oMfHBJZfHAJ55TRWqJv9hSpR38".parse()?, +//! }, +//! AccountWithPublicKey { +//! account_id: "rpc_docs.testnet".parse()?, +//! public_key: "ed25519:FxGiXr6Dgn92kqBqbQzuoYdKngiizCnywpaN7ALar3Vv".parse()?, +//! } +//! +//! ], +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_changes::RpcStateChangesInBlockResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! - `AllAccessKeyChanges` +//! +//! ``` +//! # use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{views::StateChangesRequestView, types::{BlockReference, BlockId}}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_changes::RpcStateChangesInBlockByTypeRequest { +//! block_reference: BlockReference::BlockId(BlockId::Hash("94yBWhN848vHMnKcw5DxgBQWJW6JHRXnXD6FCLJGjxMU".parse()?)), +//! state_changes_request: StateChangesRequestView::AllAccessKeyChanges { +//! account_ids: vec!["fido.testnet".parse()?, "rpc_docs.testnet".parse()?], +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_changes::RpcStateChangesInBlockResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! - `ContractCodeChanges` +//! +//! ``` +//! # use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{views::StateChangesRequestView, types::{BlockReference, BlockId}}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_changes::RpcStateChangesInBlockByTypeRequest { +//! block_reference: BlockReference::BlockId(BlockId::Hash("94yBWhN848vHMnKcw5DxgBQWJW6JHRXnXD6FCLJGjxMU".parse()?)), +//! state_changes_request: StateChangesRequestView::ContractCodeChanges { +//! account_ids: vec!["fido.testnet".parse()?, "rpc_docs.testnet".parse()?], +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_changes::RpcStateChangesInBlockResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! - `DataChanges` +//! +//! ``` +//! # use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{views::StateChangesRequestView, types::{BlockReference, BlockId, StoreKey}, hash::CryptoHash}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_changes::RpcStateChangesInBlockByTypeRequest { +//! block_reference: BlockReference::BlockId(BlockId::Hash("94yBWhN848vHMnKcw5DxgBQWJW6JHRXnXD6FCLJGjxMU".parse::()?)), +//! state_changes_request: StateChangesRequestView::DataChanges { +//! account_ids: vec!["fido.testnet".parse()?, "rpc_docs.testnet".parse()?], +//! key_prefix: StoreKey::from(vec![]), +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_changes::RpcStateChangesInBlockResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::changes::{ diff --git a/src/methods/experimental/changes_in_block.rs b/src/methods/experimental/changes_in_block.rs index 3bc7f09e..03d907be 100644 --- a/src/methods/experimental/changes_in_block.rs +++ b/src/methods/experimental/changes_in_block.rs @@ -1,3 +1,34 @@ +//! Returns the changes in a block. +//! +//! The `RpcStateChangesInBlockRequest` takes in a [`BlockReference`](https://docs.rs/near-primitives/0.12.0/near_primitives/types/enum.BlockReference.html) enum which has multiple variants. +//! +//! ## Example +//! +//! Returns the changes in block for +//! +//! You can also use the `Finality` and `SyncCheckpoint` variants of [`BlockReference`](https://docs.rs/near-primitives/0.12.0/near_primitives/types/enum.BlockReference.html) to return block change details. +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::types::{BlockReference, BlockId}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_changes_in_block::RpcStateChangesInBlockRequest { +//! block_reference: BlockReference::BlockId(BlockId::Height(47988413)) +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_changes_in_block::RpcStateChangesInBlockByTypeResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::changes::{ diff --git a/src/methods/experimental/check_tx.rs b/src/methods/experimental/check_tx.rs index 1b71fc30..a75abc41 100644 --- a/src/methods/experimental/check_tx.rs +++ b/src/methods/experimental/check_tx.rs @@ -1,3 +1,57 @@ +//! Checks a transaction on the network. +//! +//! This code sample doesn't make any request to the RPC node. It's been truncated for brevity sake. +//! +//! An example detailing how to construct a complete request can be found at [`contract_change_method`](https://github.com/near/near-jsonrpc-client-rs/blob/master/examples/contract_change_method.rs). +//! +//! ## Example +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_jsonrpc_primitives::types::{query::QueryResponseKind, transactions}; +//! use near_primitives::types::{AccountId, BlockReference}; +//! use near_primitives::transaction::{Action, Transaction, FunctionCallAction}; +//! use near_crypto::SecretKey; +//! use core::str::FromStr; +//! use serde_json::json; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let signer_account_id = "fido.testnet".parse::()?; +//! let signer_secret_key = SecretKey::from_str("ed25519:12dhevYshfiRqFSu8DSfxA27pTkmGRv6C5qQWTJYTcBEoB7MSTyidghi5NWXzWqrxCKgxVx97bpXPYQxYN5dieU")?; // Replace secret_key with valid signer_secret_key +//! +//! let signer = near_crypto::InMemorySigner::from_secret_key(signer_account_id, signer_secret_key); +//! +//! let other_account = "rpc_docs.testnet".parse::()?; +//! let rating = "4.7".parse::()?; +//! +//! let transaction = Transaction { +//! signer_id: signer.account_id.clone(), +//! public_key: signer.public_key.clone(), +//! nonce: 904565 + 1, +//! receiver_id: "nosedive.testnet".parse::()?, +//! block_hash: "AUDcb2iNUbsmCsmYGfGuKzyXKimiNcCZjBKTVsbZGnoH".parse()?, +//! actions: vec![Action::FunctionCall(FunctionCallAction { +//! method_name: "rate".to_string(), +//! args: json!({ +//! "account_id": other_account, +//! "rating": rating, +//! }) +//! .to_string() +//! .into_bytes(), +//! gas: 100_000_000_000_000, // 100 TeraGas +//! deposit: 0, +//! })], +//! }; +//! +//! let request = methods::EXPERIMENTAL_check_tx::RpcCheckTxRequest { +//! signed_transaction: transaction.sign(&signer) +//! }; +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::transactions::{ @@ -11,11 +65,12 @@ pub struct RpcCheckTxRequest { } impl From - for near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest + for near_jsonrpc_primitives::types::transactions::RpcSendTransactionRequest { fn from(this: RpcCheckTxRequest) -> Self { Self { signed_transaction: this.signed_transaction, + wait_until: near_primitives::views::TxExecutionStatus::None, } } } diff --git a/src/methods/experimental/genesis_config.rs b/src/methods/experimental/genesis_config.rs index 1cbd659c..e460aebf 100644 --- a/src/methods/experimental/genesis_config.rs +++ b/src/methods/experimental/genesis_config.rs @@ -1,3 +1,27 @@ +//! Queries the genesis config of the network. +//! +//! ## Example +//! +//! Returns the genesis config of the network. +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://rpc.mainnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_genesis_config::RpcGenesisConfigRequest; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_genesis_config::RpcGenesisConfigResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub type RpcGenesisConfigResponse = near_chain_configs::GenesisConfig; diff --git a/src/methods/experimental/protocol_config.rs b/src/methods/experimental/protocol_config.rs index 7fcb014f..a7817bee 100644 --- a/src/methods/experimental/protocol_config.rs +++ b/src/methods/experimental/protocol_config.rs @@ -1,3 +1,32 @@ +//! Queries the protocol config of the blockchain at a given block. +//! +//! The `RpcProtocolConfigRequest` takes in a [`BlockReference`](https://docs.rs/near-primitives/0.12.0/near_primitives/types/enum.BlockReference.html) enum which has multiple variants. +//! +//! ## Example +//! +//! Returns the protocol config of the blockchain at a given block. +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::types::{BlockReference, BlockId}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_protocol_config::RpcProtocolConfigRequest { +//! block_reference: BlockReference::BlockId(BlockId::Height(47988413)) +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_protocol_config::RpcProtocolConfigResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::config::{ @@ -9,7 +38,7 @@ pub type RpcProtocolConfigResponse = near_chain_configs::ProtocolConfigView; impl RpcHandlerResponse for RpcProtocolConfigResponse {} impl RpcHandlerError for RpcProtocolConfigError { - fn parse_raw_error(value: serde_json::Value) -> Option> { + fn parse(value: serde_json::Value) -> Result { common::parse_unknown_block!(value => Self) } } diff --git a/src/methods/experimental/receipt.rs b/src/methods/experimental/receipt.rs index b59f714e..9139976d 100644 --- a/src/methods/experimental/receipt.rs +++ b/src/methods/experimental/receipt.rs @@ -1,3 +1,34 @@ +//! Fetches a receipt by it's ID +//! +//! The `RpcReceiptRequest` takes in a [`ReceiptReference`](https://docs.rs/near-jsonrpc-primitives/0.12.0/near_jsonrpc_primitives/types/receipts/struct.ReceiptReference.html) +//! +//! ## Example +//! +//! Returns the receipt for this [transaction](https://explorer.near.org/transactions/4nVcmhWkV8Y3uJp9VQWrJhfesncJERfrvt9WwDi77oEJ#3B5PPT9EKj5352Wks9GnCeSUBDsVvSF4ceMQv2nEULTf) on mainnet. +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_jsonrpc_primitives::types::receipts::ReceiptReference; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_receipt::RpcReceiptRequest { +//! receipt_reference: ReceiptReference { +//! receipt_id: "3B5PPT9EKj5352Wks9GnCeSUBDsVvSF4ceMQv2nEULTf".parse()?, +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_receipt::RpcReceiptResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::receipts::{RpcReceiptError, RpcReceiptRequest}; diff --git a/src/methods/experimental/tx_status.rs b/src/methods/experimental/tx_status.rs index 26e6b29c..2bbbb8e6 100644 --- a/src/methods/experimental/tx_status.rs +++ b/src/methods/experimental/tx_status.rs @@ -1,3 +1,35 @@ +//! Queries the status of a transaction. +//! +//! ## Example +//! +//! Returns the final transaction result for +//! +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::views; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! let tx_hash = "B9aypWiMuiWR5kqzewL9eC96uZWA3qCMhLe67eBMWacq".parse()?; +//! +//! let request = methods::EXPERIMENTAL_tx_status::RpcTransactionStatusRequest { +//! transaction_info: methods::EXPERIMENTAL_tx_status::TransactionInfo::TransactionId { +//! hash: tx_hash, +//! account_id: "itranscend.near".parse()?, +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! views::FinalExecutionOutcomeWithReceiptView { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::transactions::RpcTransactionError; @@ -12,11 +44,12 @@ pub struct RpcTransactionStatusRequest { } impl From - for near_jsonrpc_primitives::types::transactions::RpcTransactionStatusCommonRequest + for near_jsonrpc_primitives::types::transactions::RpcTransactionStatusRequest { fn from(this: RpcTransactionStatusRequest) -> Self { Self { transaction_info: this.transaction_info, + wait_until: near_primitives::views::TxExecutionStatus::None, } } } @@ -34,10 +67,14 @@ impl RpcMethod for RpcTransactionStatusRequest { fn params(&self) -> Result { Ok(match &self.transaction_info { TransactionInfo::Transaction(signed_transaction) => { - json!([common::serialize_signed_transaction(signed_transaction)?]) + match signed_transaction { + near_jsonrpc_primitives::types::transactions::SignedTransaction::SignedTransaction(tx) => { + json!([common::serialize_signed_transaction(tx)?]) + }, + } } - TransactionInfo::TransactionId { hash, account_id } => { - json!([hash, account_id]) + TransactionInfo::TransactionId { tx_hash,sender_account_id } => { + json!([tx_hash, sender_account_id]) } }) } diff --git a/src/methods/experimental/validators_ordered.rs b/src/methods/experimental/validators_ordered.rs index b2b0f58c..2833787e 100644 --- a/src/methods/experimental/validators_ordered.rs +++ b/src/methods/experimental/validators_ordered.rs @@ -1,3 +1,30 @@ +//! Returns the ordered validators of a block. +//! +//! ## Example +//! +//! Returns the ordered validators for this [block](https://explorer.near.org/blocks/3Lq3Mtfpc3spH9oF5dXnUzvCBEqjTQwX1yCqKibwzgWR). +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::types::BlockId; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::EXPERIMENTAL_validators_ordered::RpcValidatorsOrderedRequest { +//! block_id: Some(BlockId::Hash("3Lq3Mtfpc3spH9oF5dXnUzvCBEqjTQwX1yCqKibwzgWR".parse()?)) +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::EXPERIMENTAL_validators_ordered::RpcValidatorsOrderedResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::validator::{ diff --git a/src/methods/gas_price.rs b/src/methods/gas_price.rs index 21c05732..a21897c5 100644 --- a/src/methods/gas_price.rs +++ b/src/methods/gas_price.rs @@ -1,3 +1,57 @@ +//! Returns the gas price for a specific block height or block hash. +//! +//! ## Examples +//! +//! Returns the gas fees for this block: +//! +//! +//! - `BlockId::Height` +//! +//! ``` +//! # use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::types::BlockId; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::gas_price::RpcGasPriceRequest { +//! block_id: Some(BlockId::Height(61512623)), +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::gas_price::RpcGasPriceResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! - `BlockId::Hash` +//! +//! ``` +//! # use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::types::BlockId; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::gas_price::RpcGasPriceRequest { +//! block_id: Some(BlockId::Hash("6atGq4TUTZerVHU9qWoYfzXNBg3K4C4cca15TE6KfuBr".parse()?)), +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::gas_price::RpcGasPriceResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::gas_price::{RpcGasPriceError, RpcGasPriceRequest}; @@ -7,7 +61,7 @@ pub type RpcGasPriceResponse = near_primitives::views::GasPriceView; impl RpcHandlerResponse for RpcGasPriceResponse {} impl RpcHandlerError for RpcGasPriceError { - fn parse_raw_error(value: serde_json::Value) -> Option> { + fn parse(value: serde_json::Value) -> Result { common::parse_unknown_block!(value => Self) } } diff --git a/src/methods/light_client_proof.rs b/src/methods/light_client_proof.rs index c48d8f4b..27ce3d1e 100644 --- a/src/methods/light_client_proof.rs +++ b/src/methods/light_client_proof.rs @@ -5,7 +5,7 @@ //! use near_primitives::types::TransactionOrReceiptId; //! //! # #[tokio::main] -//! # async fn main() -> Result<(), Box> { +//! # async fn main() -> Result<(), Box> { //! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); //! //! let request = methods::light_client_proof::RpcLightClientExecutionProofRequest { @@ -35,7 +35,7 @@ pub use near_jsonrpc_primitives::types::light_client::{ impl RpcHandlerResponse for RpcLightClientExecutionProofResponse {} impl RpcHandlerError for RpcLightClientProofError { - fn parse_raw_error(value: serde_json::Value) -> Option> { + fn parse(value: serde_json::Value) -> Result { common::parse_unknown_block!(value => Self) } } diff --git a/src/methods/mod.rs b/src/methods/mod.rs index 7a120f46..f4e7a2ea 100644 --- a/src/methods/mod.rs +++ b/src/methods/mod.rs @@ -1,3 +1,4 @@ +//! This module contains all the RPC methods. use std::io; use serde::Deserialize; @@ -8,6 +9,7 @@ mod private { pub trait Sealed {} } +/// A trait identifying valid NEAR JSON-RPC methods. pub trait RpcMethod: private::Sealed where Self::Response: RpcHandlerResponse, @@ -42,14 +44,22 @@ where fn params(&self) -> Result { T::params(self) } + + fn parse_handler_response( + response: serde_json::Value, + ) -> Result, serde_json::Error> { + T::parse_handler_response(response) + } } +/// A trait identifying valid NEAR JSON-RPC method responses. pub trait RpcHandlerResponse: serde::de::DeserializeOwned { fn parse(value: serde_json::Value) -> Result { serde_json::from_value(value) } } +/// A trait identifying valid NEAR JSON-RPC errors. pub trait RpcHandlerError: serde::de::DeserializeOwned { /// Parser for the `.error_struct` field in RpcError. fn parse(handler_error: serde_json::Value) -> Result { @@ -61,7 +71,7 @@ pub trait RpcHandlerError: serde::de::DeserializeOwned { /// This would only ever be used as a fallback if [`RpcHandlerError::parse`] fails. /// /// Defaults to `None` meaning there's no alternative deserialization available. - fn parse_raw_error(_error: serde_json::Value) -> Option> { + fn parse_legacy_error(_error: serde_json::Value) -> Option> { None } } @@ -136,10 +146,11 @@ pub use adversarial::adv_get_saved_blocks; pub use adversarial::adv_check_store; // ======== adversarial ======== +/// Converts an RPC Method into JSON. pub fn to_json(method: &M) -> Result { let request_payload = near_jsonrpc_primitives::message::Message::request( method.method_name().to_string(), - Some(method.params()?), + method.params()?, ); Ok(json!(request_payload)) @@ -153,12 +164,16 @@ mod common { // their UnknownBlock variants. macro_rules! _parse_unknown_block { ($json:expr => $err_ty:ident) => { - if $json["name"] == "UNKNOWN_BLOCK" { - Some(Ok($err_ty::UnknownBlock { - error_message: "".to_string(), - })) - } else { - None + match $json { + err => { + if err["name"] == "UNKNOWN_BLOCK" { + Ok($err_ty::UnknownBlock { + error_message: "".to_string(), + }) + } else { + serde_json::from_value(err) + } + } } }; } @@ -167,9 +182,7 @@ mod common { pub fn serialize_signed_transaction( tx: &near_primitives::transaction::SignedTransaction, ) -> Result { - Ok(near_primitives::serialize::to_base64( - &borsh::BorshSerialize::try_to_vec(&tx)?, - )) + Ok(near_primitives::serialize::to_base64(&borsh::to_vec(&tx)?)) } // adv_* @@ -203,7 +216,7 @@ mod common { // broadcast_tx_commit, tx, EXPERIMENTAL_check_tx, EXPERIMENTAL_tx_status impl RpcHandlerError for near_jsonrpc_primitives::types::transactions::RpcTransactionError { - fn parse_raw_error(value: serde_json::Value) -> Option> { + fn parse_legacy_error(value: serde_json::Value) -> Option> { match serde_json::from_value::(value) { Ok(near_jsonrpc_primitives::errors::ServerError::TxExecutionError( near_primitives::errors::TxExecutionError::InvalidTxError(context), @@ -219,7 +232,7 @@ mod common { // EXPERIMENTAL_changes, EXPERIMENTAL_changes_in_block impl RpcHandlerError for near_jsonrpc_primitives::types::changes::RpcStateChangesError { - fn parse_raw_error(value: serde_json::Value) -> Option> { + fn parse(value: serde_json::Value) -> Result { parse_unknown_block!(value => Self) } } diff --git a/src/methods/next_light_client_block.rs b/src/methods/next_light_client_block.rs index a6933bd1..63502b67 100644 --- a/src/methods/next_light_client_block.rs +++ b/src/methods/next_light_client_block.rs @@ -1,3 +1,27 @@ +//! Returns the next light client block. +//! +//! ## Example +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::next_light_client_block::RpcLightClientNextBlockRequest { +//! last_block_hash: "ANm3jm5wq1Z4rJv6tXWyiDtC3wYKpXVHY4iq6bE1te7B".parse()?, +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! Some(methods::next_light_client_block::LightClientBlockView { .. }) +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::light_client::{ @@ -10,7 +34,7 @@ pub type RpcLightClientNextBlockResponse = Option; impl RpcHandlerResponse for RpcLightClientNextBlockResponse {} impl RpcHandlerError for RpcLightClientNextBlockError { - fn parse_raw_error(value: serde_json::Value) -> Option> { + fn parse(value: serde_json::Value) -> Result { common::parse_unknown_block!(value => Self) } } diff --git a/src/methods/query.rs b/src/methods/query.rs index 1e63577e..439fc96c 100644 --- a/src/methods/query.rs +++ b/src/methods/query.rs @@ -1,3 +1,194 @@ +//! This module allows you to make generic requests to the network. +//! +//! The `RpcQueryRequest` struct takes in a [`BlockReference`](https://docs.rs/near-primitives/0.12.0/near_primitives/types/enum.BlockReference.html) and a [`QueryRequest`](https://docs.rs/near-primitives/0.12.0/near_primitives/views/enum.QueryRequest.html). +//! +//! The `BlockReference` enum allows you to specify a block by `Finality`, `BlockId` or `SyncCheckpoint`. +//! +//! The `QueryRequest` enum provides multiple variaints for performing the following actions: +//! - View an account's details +//! - View a contract's code +//! - View the state of an account +//! - View the `AccessKey` of an account +//! - View the `AccessKeyList` of an account +//! - Call a function in a contract deployed on the network. +//! +//! ## Examples +//! +//! ### Returns basic account information. +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{types::{BlockReference, BlockId}, views::QueryRequest}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::query::RpcQueryRequest { +//! block_reference: BlockReference::BlockId(BlockId::Hash("6Qq9hYG7vQhnje4iC1hfbyhh9vNQoNem7j8Dxi7EVSdN".parse()?)), +//! request: QueryRequest::ViewAccount { +//! account_id: "itranscend.near".parse()?, +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::query::RpcQueryResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Returns the contract code (Wasm binary) deployed to the account. The returned code will be encoded in base64. +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{types::{BlockReference, BlockId}, views::QueryRequest}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::query::RpcQueryRequest { +//! block_reference: BlockReference::BlockId(BlockId::Hash("CrYzVUyam5TMJTcJDJMSJ7Fzc79SDTgtK1SfVpEnteZF".parse()?)), +//! request: QueryRequest::ViewCode { +//! account_id: "nosedive.testnet".parse()?, +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::query::RpcQueryResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Returns the account state +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{types::{BlockReference, BlockId, StoreKey}, views::QueryRequest}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::query::RpcQueryRequest { +//! // block_reference: BlockReference::BlockId(BlockId::Hash("AUDcb2iNUbsmCsmYGfGuKzyXKimiNcCZjBKTVsbZGnoH".parse()?)), +//! block_reference: BlockReference::latest(), +//! request: QueryRequest::ViewState { +//! account_id: "nosedive.testnet".parse()?, +//! prefix: StoreKey::from(vec![]) +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::query::RpcQueryResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Returns information about a single access key for given account +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{types::{BlockReference, BlockId}, views::QueryRequest}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::query::RpcQueryRequest { +//! // block_reference: BlockReference::BlockId(BlockId::Hash("CA9bigchLBUYKaHKz3vQxK3Z7Fae2gnVabGrrLJrQEzp".parse()?)), +//! block_reference: BlockReference::latest(), +//! request: QueryRequest::ViewAccessKey { +//! account_id: "fido.testnet".parse()?, +//! public_key: "ed25519:GwRkfEckaADh5tVxe3oMfHBJZfHAJ55TRWqJv9hSpR38".parse()? +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::query::RpcQueryResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Returns all access keys for a given account. +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{types::{BlockReference, BlockId}, views::QueryRequest}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::query::RpcQueryRequest { +//! block_reference: BlockReference::BlockId(BlockId::Hash("AUDcb2iNUbsmCsmYGfGuKzyXKimiNcCZjBKTVsbZGnoH".parse()?)), +//! request: QueryRequest::ViewAccessKeyList { +//! account_id: "nosedive.testnet".parse()?, +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::query::RpcQueryResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Call a function in a contract deployed on the network +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{types::{BlockReference, BlockId, FunctionArgs}, views::QueryRequest}; +//! use serde_json::json; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); +//! +//! let request = methods::query::RpcQueryRequest { +//! // block_reference: BlockReference::BlockId(BlockId::Hash("CA9bigchLBUYKaHKz3vQxK3Z7Fae2gnVabGrrLJrQEzp".parse()?)), +//! block_reference: BlockReference::latest(), +//! request: QueryRequest::CallFunction { +//! account_id: "nosedive.testnet".parse()?, +//! method_name: "status".parse()?, +//! args: FunctionArgs::from( +//! json!({ +//! "account_id": "miraclx.testnet", +//! }) +//! .to_string() +//! .into_bytes(), +//! ) +//! } +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::query::RpcQueryResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::query::{RpcQueryError, RpcQueryRequest, RpcQueryResponse}; @@ -92,6 +283,37 @@ struct LegacyQueryError { mod tests { use {super::*, crate::*}; + /// This test is to make sure the method executor treats `&RpcMethod`s the same as `RpcMethod`s. + #[tokio::test] + async fn test_unknown_method() -> Result<(), Box> { + let client = JsonRpcClient::connect("https://rpc.testnet.near.org"); + + let request = RpcQueryRequest { + block_reference: near_primitives::types::BlockReference::latest(), + request: near_primitives::views::QueryRequest::CallFunction { + account_id: "testnet".parse()?, + method_name: "some_unavailable_method".to_string(), + args: vec![].into(), + }, + }; + + let response_err = client.call(&request).await.unwrap_err(); + + assert!( + matches!( + response_err.handler_error(), + Some(RpcQueryError::ContractExecutionError { + ref vm_error, + .. + }) if vm_error.contains("MethodResolveError(MethodNotFound)") + ), + "this is unexpected: {:#?}", + response_err + ); + + Ok(()) + } + #[tokio::test] async fn test_unknown_access_key() -> Result<(), Box> { let client = JsonRpcClient::connect("https://archival-rpc.testnet.near.org"); @@ -106,17 +328,20 @@ mod tests { }, }; - let response = client.call(request).await.unwrap_err(); + let response_err = client.call(request).await.unwrap_err(); - let err = response.handler_error()?; - assert!(matches!( - err, - RpcQueryError::UnknownAccessKey { - ref public_key, - block_height: 63503911, - .. - } if public_key.to_string() == "ed25519:9KnjTjL6vVoM8heHvCcTgLZ67FwFkiLsNtknFAVsVvYY" - ),); + assert!( + matches!( + response_err.handler_error(), + Some(RpcQueryError::UnknownAccessKey { + ref public_key, + block_height: 63503911, + .. + }) if public_key.to_string() == "ed25519:9KnjTjL6vVoM8heHvCcTgLZ67FwFkiLsNtknFAVsVvYY" + ), + "this is unexpected: {:#?}", + response_err + ); Ok(()) } @@ -130,27 +355,25 @@ mod tests { near_primitives::types::BlockId::Height(63503911), ), request: near_primitives::views::QueryRequest::CallFunction { - #[allow(deprecated)] account_id: "miraclx.testnet".parse()?, method_name: "".to_string(), args: vec![].into(), }, }; - let response = client.call(request).await.unwrap_err(); + let response_err = client.call(request).await.unwrap_err(); - let err = response.handler_error()?; assert!( matches!( - err, - RpcQueryError::ContractExecutionError { + response_err.handler_error(), + Some(RpcQueryError::ContractExecutionError { ref vm_error, block_height: 63503911, .. - } if vm_error.contains("FunctionCallError(MethodResolveError(MethodEmptyName))") + }) if vm_error.contains("MethodResolveError(MethodEmptyName)") ), - "{:?}", - err + "this is unexpected: {:#?}", + response_err ); Ok(()) diff --git a/src/methods/sandbox/fast_forward.rs b/src/methods/sandbox/fast_forward.rs index 0bd764b7..be16fb1e 100644 --- a/src/methods/sandbox/fast_forward.rs +++ b/src/methods/sandbox/fast_forward.rs @@ -1,3 +1,29 @@ +//! Fast forwards a sandboxed node by a specific height. +//! +//! Fas forwarding allows one to skip to some point in the future and observe actions. +//! +//! ## Example +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("http://localhost:3030"); +//! +//! let request = methods::sandbox_fast_forward::RpcSandboxFastForwardRequest { +//! delta_height: 12, +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::sandbox_fast_forward::RpcSandboxFastForwardResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::sandbox::{ diff --git a/src/methods/sandbox/patch_state.rs b/src/methods/sandbox/patch_state.rs index 25f16290..0015e7b7 100644 --- a/src/methods/sandbox/patch_state.rs +++ b/src/methods/sandbox/patch_state.rs @@ -1,3 +1,37 @@ +//! Patch account, access keys, contract code, or contract state. +//! +//! Only additions and mutations are supported. No deletions. +//! +//! Account, access keys, contract code, and contract states have different formats. See the example and docs for details about their format. +//! +//! ## Examples +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::{state_record::StateRecord, account, types::AccountId, hash::CryptoHash}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("http://localhost:3030"); +//! +//! let request = methods::sandbox_patch_state::RpcSandboxPatchStateRequest { +//! records: vec![ +//! StateRecord::Account { +//! account_id: "fido.testnet".parse::()?, +//! account: account::Account::new(179, 0, CryptoHash::default(), 264) +//! } +//! ], +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::sandbox_patch_state::RpcSandboxPatchStateResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::sandbox::{ diff --git a/src/methods/tx.rs b/src/methods/tx.rs index 5a217eee..7e441b7c 100644 --- a/src/methods/tx.rs +++ b/src/methods/tx.rs @@ -8,14 +8,14 @@ //! use near_jsonrpc_client::{methods, JsonRpcClient}; //! //! # #[tokio::main] -//! # async fn main() -> Result<(), Box> { +//! # async fn main() -> Result<(), Box> { //! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); //! let tx_hash = "B9aypWiMuiWR5kqzewL9eC96uZWA3qCMhLe67eBMWacq".parse()?; //! //! let request = methods::tx::RpcTransactionStatusRequest { //! transaction_info: methods::tx::TransactionInfo::TransactionId { -//! hash: tx_hash, -//! account_id: "itranscend.near".parse()?, +//! tx_hash, +//! sender_account_id: "itranscend.near".parse()?, //! } //! }; //! @@ -38,11 +38,12 @@ pub struct RpcTransactionStatusRequest { } impl From - for near_jsonrpc_primitives::types::transactions::RpcTransactionStatusCommonRequest + for near_jsonrpc_primitives::types::transactions::RpcTransactionStatusRequest { fn from(this: RpcTransactionStatusRequest) -> Self { Self { transaction_info: this.transaction_info, + wait_until: near_primitives::views::TxExecutionStatus::None, } } } @@ -58,10 +59,14 @@ impl RpcMethod for RpcTransactionStatusRequest { fn params(&self) -> Result { Ok(match &self.transaction_info { TransactionInfo::Transaction(signed_transaction) => { - json!([common::serialize_signed_transaction(signed_transaction)?]) + match signed_transaction { + near_jsonrpc_primitives::types::transactions::SignedTransaction::SignedTransaction(tx) => { + json!([common::serialize_signed_transaction(tx)?]) + } + } } - TransactionInfo::TransactionId { hash, account_id } => { - json!([hash, account_id]) + TransactionInfo::TransactionId { tx_hash,sender_account_id, ..} => { + json!([tx_hash, sender_account_id]) } }) } diff --git a/src/methods/validators.rs b/src/methods/validators.rs index db3467b2..7770e75b 100644 --- a/src/methods/validators.rs +++ b/src/methods/validators.rs @@ -1,3 +1,58 @@ +//! Queries active validators on the network. +//! +//! Returns details and the state of validation on the blockchain. +//! +//! ## Examples +//! +//! - Get the validators for a specified epoch. +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::types::{EpochReference, EpochId, BlockReference, Finality}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::validators::RpcValidatorRequest { +//! epoch_reference: EpochReference::EpochId(EpochId { +//! 0: "9xrjdZmgjoVkjVE3ui7tY37x9Mkw5wH385qNXE6cho7T".parse()?, +//! }) +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::validators::RpcValidatorResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` +//! +//! - Get the validators for the latest block. +//! +//! ``` +//! use near_jsonrpc_client::{methods, JsonRpcClient}; +//! use near_primitives::types::{EpochReference, EpochId, BlockId}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org"); +//! +//! let request = methods::validators::RpcValidatorRequest { +//! epoch_reference: EpochReference::Latest +//! }; +//! +//! let response = client.call(request).await?; +//! +//! assert!(matches!( +//! response, +//! methods::validators::RpcValidatorResponse { .. } +//! )); +//! # Ok(()) +//! # } +//! ``` use super::*; pub use near_jsonrpc_primitives::types::validator::{RpcValidatorError, RpcValidatorRequest};