mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-10-20 18:56:19 +00:00
Merge remote-tracking branch 'origin/master' into lexnv/codegen-config
This commit is contained in:
111 files changed
+24479
-7527
No files matched your search
@@ -1,7 +1,8 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
directories:
|
||||
- "**/*"
|
||||
schedule:
|
||||
interval: weekly
|
||||
ignore:
|
||||
@@ -13,6 +14,6 @@ updates:
|
||||
- dependency-name: sp-crypto-hashing
|
||||
- dependency-name: sp-version
|
||||
- package-ecosystem: github-actions
|
||||
directory: '/'
|
||||
directory: "**/*"
|
||||
schedule:
|
||||
interval: weekly
|
||||
+14
-14
@@ -47,7 +47,7 @@ jobs:
|
||||
args: --all -- --check
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
machete:
|
||||
name: "Check unused dependencies"
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
command: machete
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
clippy:
|
||||
name: Cargo clippy
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
cargo clippy -p subxt --no-default-features --features web,unstable-light-client -- -D warnings
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
wasm_clippy:
|
||||
name: Cargo clippy (WASM)
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
args: -p subxt --no-default-features --features web,unstable-light-client,jsonrpsee --target wasm32-unknown-unknown -- -D warnings
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
check:
|
||||
name: Cargo check
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
run: cargo check --manifest-path examples/parachain-example/Cargo.toml
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
wasm_check:
|
||||
name: Cargo check (WASM)
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
cargo check --manifest-path examples/wasm-example/Cargo.toml --target wasm32-unknown-unknown
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
docs:
|
||||
name: Check documentation and run doc tests
|
||||
@@ -261,10 +261,10 @@ jobs:
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: test
|
||||
args: --doc
|
||||
args: --doc --features unstable-reconnecting-rpc-client
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
tests:
|
||||
name: "Test (Native)"
|
||||
@@ -295,10 +295,10 @@ jobs:
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: nextest
|
||||
args: run --workspace
|
||||
args: run --workspace --features unstable-reconnecting-rpc-client
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
unstable_backend_tests:
|
||||
name: "Test (Unstable Backend)"
|
||||
@@ -332,7 +332,7 @@ jobs:
|
||||
args: run --workspace --features unstable-backend-client
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
light_client_tests:
|
||||
name: "Test (Light Client)"
|
||||
@@ -363,7 +363,7 @@ jobs:
|
||||
args: --release --package integration-tests --features unstable-light-client
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
wasm_tests:
|
||||
name: Test (WASM)
|
||||
@@ -419,7 +419,7 @@ jobs:
|
||||
working-directory: signer/wasm-tests
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
no-std-tests:
|
||||
name: "Test (no_std)"
|
||||
@@ -453,4 +453,4 @@ jobs:
|
||||
working-directory: testing/no-std-tests
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@271cfbfa11ca9222f7be99a47e8f929574549e0a" # v0.4
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
private-key: ${{ secrets.SUBXT_PR_MAKER_APP_KEY }}
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
base: master
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Dependabot
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/dependabot.yml'
|
||||
- '.github/workflows/validate-dependabot.yml'
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: marocchino/validate-dependabot@v3
|
||||
id: validate
|
||||
+124
@@ -4,6 +4,130 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.37.0] - 2024-05-28
|
||||
|
||||
This release mainly adds support for the sign extension `CheckMetadataHash` and fixes a regression introduced in v0.36.0
|
||||
where the type de-duplication was too aggressive and lots of the same type such as `BoundedVec` was duplicated to
|
||||
plenty of different types such as BoundedVec1, BoundedVec2, .. BoundedVec<N>.
|
||||
|
||||
### Added
|
||||
- Implemented `sign_prehashed` for `ecdsa::Keypair` and `eth::Keypair` ([#1598](https://github.com/paritytech/subxt/pull/1598))
|
||||
- Add a basic version of the CheckMetadataHash signed extension ([#1590](https://github.com/paritytech/subxt/pull/1590))
|
||||
|
||||
## Changed
|
||||
- Remove `derive_more` ([#1600](https://github.com/paritytech/subxt/pull/1600))
|
||||
- chore(deps): bump scale-typegen v0.8.0 ([#1615](https://github.com/paritytech/subxt/pull/1615))
|
||||
|
||||
## [0.36.1] - 2024-05-28 [YANKED]
|
||||
|
||||
Yanked because the typegen changed, it's a breaking change.
|
||||
|
||||
## [0.36.0] - 2024-05-16
|
||||
|
||||
This release adds a few new features, which I'll go over below in more detail.
|
||||
|
||||
### [`subxt-core`](https://github.com/paritytech/subxt/pull/1508)
|
||||
|
||||
We now have a brand new `subxt-core` crate, which is `#[no-std]` compatible, and contains a lot of the core logic that is needed in Subxt. Using this crate, you can do things in a no-std environment like:
|
||||
|
||||
- `blocks`: decode and explore block bodies.
|
||||
- `constants`: access and validate the constant addresses in some metadata.
|
||||
- `custom_values`: access and validate the custom value addresses in some metadata.
|
||||
- `metadata`: decode bytes into the metadata used throughout this library.
|
||||
- `storage`: construct storage request payloads and decode the results you'd get back.
|
||||
- `tx`: construct and sign transactions (extrinsics).
|
||||
- `runtime_api`: construct runtime API request payloads and decode the results you'd get back.
|
||||
- `events`: decode and explore events.
|
||||
|
||||
Check out [the docs](https://docs.rs/subxt-core/latest/subxt_core/) for more, including examples of each case.
|
||||
|
||||
A breaking change that comes from migrating a bunch of logic to this new crate is that the `ExtrinsicParams` trait is now handed `&ClientState<T>` rather than a `Client`. `ClientState` is just a concrete struct containing the state that one needs for things like signed extensions.
|
||||
|
||||
### [Support for reconnecting](https://github.com/paritytech/subxt/pull/1505)
|
||||
|
||||
We've baked in a bunch of support for automatically reconnecting after a connection loss into Subxt. This comes in three parts:
|
||||
1. An RPC client that is capable of reconnecting. This is gated behind the `unstable-reconnecting-rpc-client` feature flag at the moment, and
|
||||
2. Handling in the subxt Backends such that when the RPC client notifies it that it is reconnecting, the backend will transparently handle this behind the scenes, or else pass on a `DisconnectedWillReconnect` error to the user where it cannot. Note that the individual `LegacyRpcMethods` and `UnstableRpcMethods` are _not_ automatically retried on reconnection. Which leads us to..
|
||||
3. A couple of util helpers (`subxt::backend::retry` and `subxt::backend::retry_stream`) which can be used in conjunction with a reconnecting RPC client to make it easy to automatically retry RPC method calls where needed.
|
||||
|
||||
We'd love feedback on this reconnecting work! To try it out, enable the `unstable-reconnecting-rpc-client` feature flag and then you can make use of this like so:
|
||||
|
||||
```rust
|
||||
use std::time::Duration;
|
||||
use futures::StreamExt;
|
||||
use subxt::backend::rpc::reconnecting_rpc_client::{Client, ExponentialBackoff};
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
// Generate an interface that we can use from the node's metadata.
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a new client with a reconnecting RPC client.
|
||||
let rpc = Client::builder()
|
||||
// We can configure the retry policy; here to an exponential backoff.
|
||||
// This API accepts an iterator of retry delays, and here we use `take`
|
||||
// to limit the number of retries.
|
||||
.retry_policy(
|
||||
ExponentialBackoff::from_millis(100)
|
||||
.max_delay(Duration::from_secs(10))
|
||||
.take(3),
|
||||
)
|
||||
.build("ws://localhost:9944".to_string())
|
||||
.await?;
|
||||
|
||||
// Use this reconnecting client when instantiating a Subxt client:
|
||||
let api: OnlineClient<PolkadotConfig> = OnlineClient::from_rpc_client(rpc.clone()).await?;
|
||||
```
|
||||
|
||||
Check out the full example [here](https://github.com/paritytech/subxt/blob/64d3aae521112c8bc7366385c54a9340185d81ac/subxt/examples/setup_reconnecting_rpc_client.rs).
|
||||
|
||||
### [Better Ethereum support](https://github.com/paritytech/subxt/pull/1501)
|
||||
|
||||
We've added built-in support for Ethereum style chains (eg Frontier and Moonbeam) in `subxt-signer`, making it easier to sign transactions for these chains now.
|
||||
|
||||
Check out a full example [here](https://github.com/paritytech/subxt/blob/327b70ac94c4d925c8529a1e301d596d7db181ea/subxt/examples/tx_basic_frontier.rs).
|
||||
|
||||
We plan to improve on this in the future, baking in better Ethereum support if possible so that it's as seamless to use `AccountId20` as it is `AccountId32`.
|
||||
|
||||
### Stabilizing the new V2 RPCs ([#1540](https://github.com/paritytech/subxt/pull/1540), [#1539](https://github.com/paritytech/subxt/pull/1539), [#1538](https://github.com/paritytech/subxt/pull/1538))
|
||||
|
||||
A bunch of the new RPCs are now stable in the spec, and have consequently been stabilized here, bringing the `unstable-backend` a step closer to being stabilized itself! We'll probably first remove the feature flag and next make it the default backend, in upcoming releases.
|
||||
|
||||
All of the notable changes in this release are as follows:
|
||||
|
||||
### Added
|
||||
|
||||
- Add `frontier/ethereum` example ([#1557](https://github.com/paritytech/subxt/pull/1557))
|
||||
- Rpc: add full support reconnecting rpc client ([#1505](https://github.com/paritytech/subxt/pull/1505))
|
||||
- Signer: ethereum implementation ([#1501](https://github.com/paritytech/subxt/pull/1501))
|
||||
- `subxt-core` crate ([#1466](https://github.com/paritytech/subxt/pull/1466))
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump scale-decode and related deps to latest ([#1583](https://github.com/paritytech/subxt/pull/1583))
|
||||
- Update Artifacts (auto-generated) ([#1577](https://github.com/paritytech/subxt/pull/1577))
|
||||
- Update deps to use `scale-type-resolver` 0.2 ([#1565](https://github.com/paritytech/subxt/pull/1565))
|
||||
- Stabilize transactionBroadcast methods ([#1540](https://github.com/paritytech/subxt/pull/1540))
|
||||
- Stabilize transactionWatch methods ([#1539](https://github.com/paritytech/subxt/pull/1539))
|
||||
- Stabilize chainHead methods ([#1538](https://github.com/paritytech/subxt/pull/1538))
|
||||
- Rename traits to remove T suffix ([#1535](https://github.com/paritytech/subxt/pull/1535))
|
||||
- Add Debug/Clone/etc for common Configs for convenience ([#1542](https://github.com/paritytech/subxt/pull/1542))
|
||||
- Unstable_rpc: Add transactionBroadcast and transactionStop ([#1497](https://github.com/paritytech/subxt/pull/1497))
|
||||
|
||||
### Fixed
|
||||
|
||||
- metadata: Fix cargo clippy ([#1574](https://github.com/paritytech/subxt/pull/1574))
|
||||
- Fixed import in `subxt-signer::eth` ([#1553](https://github.com/paritytech/subxt/pull/1553))
|
||||
- chore: fix typos and link broken ([#1541](https://github.com/paritytech/subxt/pull/1541))
|
||||
- Make subxt-core ready for publishing ([#1508](https://github.com/paritytech/subxt/pull/1508))
|
||||
- Remove dupe storage item if we get one back, to be compatible with Smoldot + legacy RPCs ([#1534](https://github.com/paritytech/subxt/pull/1534))
|
||||
- fix: substrate runner libp2p port ([#1533](https://github.com/paritytech/subxt/pull/1533))
|
||||
- Swap BinaryHeap for Vec to avoid Ord constraint issue ([#1523](https://github.com/paritytech/subxt/pull/1523))
|
||||
- storage_type: Strip key proper hash and entry bytes (32 instead of 16) ([#1522](https://github.com/paritytech/subxt/pull/1522))
|
||||
- testing: Prepare light client testing with substrate binary and add subxt-test macro ([#1507](https://github.com/paritytech/subxt/pull/1507))
|
||||
|
||||
## [0.35.0] - 2024-03-21
|
||||
|
||||
This release contains several fixes, adds `no_std` support to a couple of crates (`subxt-signer` and `subxt-metadata`) and introduces a few quality of life improvements, which I'll quickly cover:
|
||||
|
||||
Generated
+1389
-385
File diff suppressed because it is too large.
Load diff
+55
-43
@@ -26,14 +26,14 @@ exclude = [
|
||||
"testing/wasm-lightclient-tests",
|
||||
"signer/wasm-tests",
|
||||
"examples/wasm-example",
|
||||
"examples/parachain-example"
|
||||
"examples/parachain-example",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2021"
|
||||
version = "0.35.0"
|
||||
version = "0.37.0"
|
||||
rust-version = "1.74.0"
|
||||
license = "Apache-2.0 OR GPL-3.0"
|
||||
repository = "https://github.com/paritytech/subxt"
|
||||
@@ -57,24 +57,27 @@ unused_extern_crates = "deny"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
type_complexity = "allow"
|
||||
all = "deny"
|
||||
# Priority -1 means that it can overwritten by other lints, https://rust-lang.github.io/rust-clippy/master/index.html#/lint_groups_priority
|
||||
all = { level = "deny", priority = -1 }
|
||||
# https://github.com/rust-lang/rust-clippy/issues/12643
|
||||
manual-unwrap-or-default = "allow"
|
||||
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.80"
|
||||
async-trait = "0.1.81"
|
||||
assert_matches = "1.5.0"
|
||||
base58 = { version = "0.2.0" }
|
||||
bitvec = { version = "1", default-features = false }
|
||||
blake2 = { version = "0.10.6", default-features = false }
|
||||
clap = { version = "4.5.3", features = ["derive", "cargo"] }
|
||||
clap = { version = "4.5.17", features = ["derive", "cargo"] }
|
||||
cfg-if = "1.0.0"
|
||||
criterion = "0.4"
|
||||
codec = { package = "parity-scale-codec", version = "3.6.9", default-features = false }
|
||||
color-eyre = "0.6.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
darling = "0.20.8"
|
||||
darling = "0.20.10"
|
||||
derive-where = "1.2.7"
|
||||
derive_more = "0.99.17"
|
||||
either = { version = "1.11.0", default-features = false }
|
||||
either = { version = "1.13.0", default-features = false }
|
||||
finito = { version = "0.1.0", default-features = false }
|
||||
frame-metadata = { version = "16.0.0", default-features = false }
|
||||
futures = { version = "0.3.30", default-features = false, features = ["std"] }
|
||||
getrandom = { version = "0.2", default-features = false }
|
||||
@@ -83,75 +86,80 @@ hex = { version = "0.4.3", default-features = false }
|
||||
heck = "0.5.0"
|
||||
impl-serde = { version = "0.4.0", default-features = false }
|
||||
indoc = "2"
|
||||
jsonrpsee = { version = "0.22" }
|
||||
jsonrpsee = { version = "0.24.3" }
|
||||
pretty_assertions = "1.4.0"
|
||||
primitive-types = { version = "0.12.2", default-features = false }
|
||||
proc-macro-error = "1.0.4"
|
||||
proc-macro2 = "1.0.81"
|
||||
quote = "1.0.36"
|
||||
regex = { version = "1.10.4", default-features = false }
|
||||
scale-info = { version = "2.11.0", default-features = false }
|
||||
scale-value = { version = "0.15.0", default-features = false }
|
||||
proc-macro-error2 = "2.0.0"
|
||||
proc-macro2 = "1.0.86"
|
||||
quote = "1.0.37"
|
||||
regex = { version = "1.10.6", default-features = false }
|
||||
scale-info = { version = "2.11.3", default-features = false }
|
||||
scale-value = { version = "0.16.2", default-features = false }
|
||||
scale-bits = { version = "0.6.0", default-features = false }
|
||||
scale-decode = { version = "0.12.0", default-features = false }
|
||||
scale-encode = { version = "0.7.0", default-features = false }
|
||||
scale-typegen = "0.5.0"
|
||||
scale-typegen-description = "0.5.0"
|
||||
serde = { version = "1.0.199", default-features = false, features = ["derive"] }
|
||||
serde_json = { version = "1.0.116", default-features = false }
|
||||
scale-decode = { version = "0.13.1", default-features = false }
|
||||
scale-encode = { version = "0.7.1", default-features = false }
|
||||
scale-typegen = "0.8.0"
|
||||
scale-typegen-description = "0.8.0"
|
||||
serde = { version = "1.0.210", default-features = false, features = ["derive"] }
|
||||
serde_json = { version = "1.0.128", default-features = false }
|
||||
syn = { version = "2.0.15", features = ["full", "extra-traits"] }
|
||||
thiserror = "1.0.59"
|
||||
tokio = { version = "1.37", default-features = false }
|
||||
thiserror = "1.0.63"
|
||||
tokio = { version = "1.40", default-features = false }
|
||||
tracing = { version = "0.1.40", default-features = false }
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing-subscriber = "0.3.18"
|
||||
trybuild = "1.0.91"
|
||||
url = "2.5.0"
|
||||
trybuild = "1.0.99"
|
||||
url = "2.5.2"
|
||||
wabt = "0.10.0"
|
||||
wasm-bindgen-test = "0.3.24"
|
||||
which = "5.0.0"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
proptest = "1.4.0"
|
||||
proptest = "1.5.0"
|
||||
hex-literal = "0.4.1"
|
||||
sc-executor = "0.40.0"
|
||||
sc-executor-common = "0.35.0"
|
||||
|
||||
# Light client support:
|
||||
smoldot = { version = "0.16.0", default-features = false }
|
||||
smoldot-light = { version = "0.14.0", default-features = false }
|
||||
tokio-stream = "0.1.15"
|
||||
smoldot = { version = "0.18.0", default-features = false }
|
||||
smoldot-light = { version = "0.16.2", default-features = false }
|
||||
tokio-stream = "0.1.16"
|
||||
futures-util = "0.3.30"
|
||||
rand = "0.8.5"
|
||||
pin-project = "1.1.5"
|
||||
|
||||
# Light client wasm:
|
||||
web-sys = { version = "0.3.69", features = ["BinaryType", "CloseEvent", "MessageEvent", "WebSocket"] }
|
||||
wasm-bindgen = "0.2.92"
|
||||
web-sys = { version = "0.3.70", features = ["BinaryType", "CloseEvent", "MessageEvent", "WebSocket"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
send_wrapper = "0.6.0"
|
||||
js-sys = "0.3.69"
|
||||
wasm-bindgen-futures = "0.4.42"
|
||||
js-sys = "0.3.70"
|
||||
wasm-bindgen-futures = "0.4.43"
|
||||
futures-timer = "3"
|
||||
instant = { version = "0.1.12", default-features = false }
|
||||
tokio-util = "0.7.10"
|
||||
instant = { version = "0.1.13", default-features = false }
|
||||
tokio-util = "0.7.12"
|
||||
|
||||
# Substrate crates:
|
||||
sp-core = { version = "31.0.0", default-features = false }
|
||||
sp-crypto-hashing = { version = "0.1.0", default-features = false }
|
||||
sp-runtime = "34.0.0"
|
||||
sp-keyring = "34.0.0"
|
||||
sp-maybe-compressed-blob = "11.0.0"
|
||||
sp-state-machine = "0.43.0"
|
||||
sp-io = "38.0.0"
|
||||
|
||||
# Subxt workspace crates:
|
||||
subxt = { version = "0.35.0", path = "subxt", default-features = false }
|
||||
subxt-core = { version = "0.35.0", path = "core", default-features = false }
|
||||
subxt-macro = { version = "0.35.0", path = "macro" }
|
||||
subxt-metadata = { version = "0.35.0", path = "metadata", default-features = false }
|
||||
subxt-codegen = { version = "0.35.0", path = "codegen" }
|
||||
subxt-signer = { version = "0.35.0", path = "signer", default-features = false }
|
||||
subxt-lightclient = { version = "0.35.0", path = "lightclient", default-features = false }
|
||||
subxt = { version = "0.37.0", path = "subxt", default-features = false }
|
||||
subxt-core = { version = "0.37.0", path = "core", default-features = false }
|
||||
subxt-macro = { version = "0.37.0", path = "macro" }
|
||||
subxt-metadata = { version = "0.37.0", path = "metadata", default-features = false }
|
||||
subxt-codegen = { version = "0.37.0", path = "codegen" }
|
||||
subxt-signer = { version = "0.37.0", path = "signer", default-features = false }
|
||||
subxt-lightclient = { version = "0.37.0", path = "lightclient", default-features = false }
|
||||
test-runtime = { path = "testing/test-runtime" }
|
||||
substrate-runner = { path = "testing/substrate-runner" }
|
||||
|
||||
# subxt-signer deps that I expect aren't useful anywhere else:
|
||||
bip39 = { version = "2.0.0", default-features = false }
|
||||
bip32 = { version = "0.5.1", default-features = false }
|
||||
bip32 = { version = "0.5.2", default-features = false }
|
||||
hmac = { version = "0.12.1", default-features = false }
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
schnorrkel = { version = "0.11.4", default-features = false }
|
||||
@@ -160,6 +168,10 @@ keccak-hash = { version = "0.10.0", default-features = false }
|
||||
secrecy = "0.8.0"
|
||||
sha2 = { version = "0.10.8", default-features = false }
|
||||
zeroize = { version = "1", default-features = false }
|
||||
base64 = { version = "0.22.1", default-features = false }
|
||||
scrypt = { version = "0.11.0", default-features = false }
|
||||
crypto_secretbox = { version = "0.1.1", default-features = false }
|
||||
|
||||
|
||||
[profile.dev.package.smoldot-light]
|
||||
opt-level = 2
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
# subxt ·  [](https://crates.io/crates/subxt) [](https://docs.rs/subxt)
|
||||
|
||||
A library to **sub**mit e**xt**rinsics to a [substrate](https://github.com/paritytech/substrate) node via RPC.
|
||||
Subxt is a library for interacting with [Substrate](https://github.com/paritytech/polkadot-sdk) based nodes in Rust and WebAssembly. It can:
|
||||
|
||||
- Submit Extrinsics (this is where the name comes from).
|
||||
- Subscribe to blocks, reading the extrinsics and associated events from them.
|
||||
- Read and iterate over storage values.
|
||||
- Read constants and custom values from the metadata.
|
||||
- Call runtime APIs, returning the results.
|
||||
- Do all of the above via a safe, statically types interface or via a dynamic one when you need the flexibility.
|
||||
- Compile to WASM and run entirely in the browser.
|
||||
- Do a bunch of things in a `#[no_std]` environment via the `subxt-core` crate.
|
||||
- Use a built-in light client (`smoldot`) to interact with chains.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -55,6 +65,7 @@ Please add your project to this list via a PR.
|
||||
- [polkadot-introspector](https://github.com/paritytech/polkadot-introspector) Tools for monitoring Polkadot nodes.
|
||||
- [ink!](https://github.com/paritytech/ink) Smart contract language that uses `subxt` for allowing developers to conduct [End-to-End testing](https://use.ink/basics/contract-testing/end-to-end-e2e-testing) of their contracts.
|
||||
- [Chainflip](https://github.com/chainflip-io/chainflip-backend) A decentralised exchange for native cross-chain swaps.
|
||||
- [Hyperbridge](https://github.com/polytope-labs/hyperbridge) A hyperscalable coprocessor for verifiable cross-chain interoperability.
|
||||
|
||||
**Alternatives**
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
"/dns/boot.stake.plus/tcp/30334/wss/p2p/12D3KooWKT4ZHNxXH4icMjdrv7EwWBkfbz5duxE5sdJKKeWFYi5n",
|
||||
"/dns/boot-node.helikon.io/tcp/7070/p2p/12D3KooWS9ZcvRxyzrSf6p63QfTCWs12nLoNKhGux865crgxVA4H",
|
||||
"/dns/boot-node.helikon.io/tcp/7072/wss/p2p/12D3KooWS9ZcvRxyzrSf6p63QfTCWs12nLoNKhGux865crgxVA4H",
|
||||
"/dns/polkadot.bootnode.amforc.com/tcp/30333/p2p/12D3KooWAsuCEVCzUVUrtib8W82Yne3jgVGhQZN3hizko5FTnDg3",
|
||||
"/dns/polkadot.bootnode.amforc.com/tcp/30334/wss/p2p/12D3KooWAsuCEVCzUVUrtib8W82Yne3jgVGhQZN3hizko5FTnDg3",
|
||||
"/dns/polkadot.bootnode.amforc.com/tcp/30001/p2p/12D3KooWT2HyZx5C6BBeLbCKhYG2SqJYuiu7sLMxGzUcQBko3BMr",
|
||||
"/dns/polkadot.bootnode.amforc.com/tcp/29999/wss/p2p/12D3KooWT2HyZx5C6BBeLbCKhYG2SqJYuiu7sLMxGzUcQBko3BMr",
|
||||
"/dns/polkadot.bootnodes.polkadotters.com/tcp/30314/p2p/12D3KooWPAVUgBaBk6n8SztLrMk8ESByncbAfRKUdxY1nygb9zG3",
|
||||
"/dns/polkadot.bootnodes.polkadotters.com/tcp/30316/wss/p2p/12D3KooWPAVUgBaBk6n8SztLrMk8ESByncbAfRKUdxY1nygb9zG3",
|
||||
"/dns/boot-cr.gatotech.network/tcp/33100/p2p/12D3KooWK4E16jKk9nRhvC4RfrDVgcZzExg8Q3Q2G7ABUUitks1w",
|
||||
"/dns/boot-cr.gatotech.network/tcp/35100/wss/p2p/12D3KooWK4E16jKk9nRhvC4RfrDVgcZzExg8Q3Q2G7ABUUitks1w",
|
||||
"/dns/boot-polkadot.metaspan.io/tcp/13012/p2p/12D3KooWRjHFApinuqSBjoaDjQHvxwubQSpEVy5hrgC9Smvh92WF",
|
||||
"/dns/boot-polkadot.metaspan.io/tcp/13015/ws/p2p/12D3KooWRjHFApinuqSBjoaDjQHvxwubQSpEVy5hrgC9Smvh92WF",
|
||||
"/dns/boot-polkadot.metaspan.io/tcp/13016/wss/p2p/12D3KooWRjHFApinuqSBjoaDjQHvxwubQSpEVy5hrgC9Smvh92WF",
|
||||
"/dns/boot.gatotech.network/tcp/33100/p2p/12D3KooWK4E16jKk9nRhvC4RfrDVgcZzExg8Q3Q2G7ABUUitks1w",
|
||||
"/dns/boot.gatotech.network/tcp/35100/wss/p2p/12D3KooWK4E16jKk9nRhvC4RfrDVgcZzExg8Q3Q2G7ABUUitks1w",
|
||||
"/dns/boot.metaspan.io/tcp/13012/p2p/12D3KooWRjHFApinuqSBjoaDjQHvxwubQSpEVy5hrgC9Smvh92WF",
|
||||
"/dns/boot.metaspan.io/tcp/13015/ws/p2p/12D3KooWRjHFApinuqSBjoaDjQHvxwubQSpEVy5hrgC9Smvh92WF",
|
||||
"/dns/boot.metaspan.io/tcp/13016/wss/p2p/12D3KooWRjHFApinuqSBjoaDjQHvxwubQSpEVy5hrgC9Smvh92WF",
|
||||
"/dns/polkadot-bootnode.turboflakes.io/tcp/30300/p2p/12D3KooWHJBMZgt7ymAdTRtadPcGXpJw79vBGe8z53r9JMkZW7Ha",
|
||||
"/dns/polkadot-bootnode.turboflakes.io/tcp/30400/wss/p2p/12D3KooWHJBMZgt7ymAdTRtadPcGXpJw79vBGe8z53r9JMkZW7Ha",
|
||||
"/dns/polkadot-boot-ng.dwellir.com/tcp/443/wss/p2p/12D3KooWFFqjBKoSdQniRpw1Y8W6kkV7takWv1DU2ZMkaA81PYVq",
|
||||
@@ -34,7 +34,9 @@
|
||||
"/dns/dot14.rotko.net/tcp/35214/wss/p2p/12D3KooWPyEvPEXghnMC67Gff6PuZiSvfx3fmziKiPZcGStZ5xff",
|
||||
"/dns/dot14.rotko.net/tcp/33214/p2p/12D3KooWPyEvPEXghnMC67Gff6PuZiSvfx3fmziKiPZcGStZ5xff",
|
||||
"/dns/ibp-boot-polkadot.luckyfriday.io/tcp/30333/p2p/12D3KooWEjk6QXrZJ26fLpaajisJGHiz6WiQsR8k7mkM9GmWKnRZ",
|
||||
"/dns/ibp-boot-polkadot.luckyfriday.io/tcp/30334/wss/p2p/12D3KooWEjk6QXrZJ26fLpaajisJGHiz6WiQsR8k7mkM9GmWKnRZ"
|
||||
"/dns/ibp-boot-polkadot.luckyfriday.io/tcp/30334/wss/p2p/12D3KooWEjk6QXrZJ26fLpaajisJGHiz6WiQsR8k7mkM9GmWKnRZ",
|
||||
"/dns/boot-polkadot.luckyfriday.io/tcp/443/wss/p2p/12D3KooWAdyiVAaeGdtBt6vn5zVetwA4z4qfm9Fi2QCSykN1wTBJ",
|
||||
"/dns4/polkadot-0.boot.onfinality.io/tcp/24446/ws/p2p/12D3KooWT1PWaNdAwYrSr89dvStnoGdH3t4LNRbcVNN4JCtsotkR"
|
||||
],
|
||||
"chainType": "Live",
|
||||
"forkBlocks": null,
|
||||
@@ -43,10 +45,10 @@
|
||||
},
|
||||
"id": "polkadot",
|
||||
"lightSyncState": {
|
||||
"babeEpochChanges": "0x0480df609c40171d0f62924a7d83db3a5c52c2f78dbe0dab0497b8786e659872e8daff340101734e031100000000d35703110000000004877d77dc713f5db844a860efb4ea0372343b3deca76cc7fa0827780217a6c4d33609350101d3570311000000003361031100000000046204e22a5bed51dced77343ac746411fa965babf73ef76b57e185eebd64f32af8d123501013361031100000000936a03110000000000000c6204e22a5bed51dced77343ac746411fa965babf73ef76b57e185eebd64f32af8d123501011f2100000000000033610311000000006009000000000000a50432904ec260963195582a1d339f9fbd2d5d76f551160105c922423cad0d94482f010000000000000076441c448aefcf40a72128edc9b44a4f7ef9c267c503e98f5f9c72d5e9e92f000100000000000000526e055ade4ac425e9b5f93ce4394601335d3599a5b742603126d78ce99a4a540100000000000000d0e8ddfdeacd8e163b01ca4da1a4e856312b0de412b492247070f2721f7d803801000000000000008008c1b7061a2402634fc15d3849e2913863beb9f55024bc8cc7fa7d6e397817010000000000000024a51a9bdb67213626d1b52bd8dbc0e9b94ea88ea417aa17b56212d1cea3e1780100000000000000e033368bc2edee96d72e43448824a642d53939c6c190693971b05ccd1054464a01000000000000004ceef9b2b8e3b5cd971f7557c3a3ad02c9c7a7bbc7365c2db5622fa74853790d0100000000000000aca7a86b6fcf25daa7f0767bfe36b284f3100dc1ee4dcc971fbb3244bb57221c01000000000000004ea4871b3ccdd1276ee604eb0e06aa4a62536dfe8fb053042a8f36caddb94c510100000000000000ba395fcb33327501109ed40712c7a372d25bf3339edfabe2a1964fb7b5d4d82401000000000000009cb389c209afbebda2b12113e0266b3d30f46e2a91aaf4451c193d640288357101000000000000004c9886e2e62ee92a6778d6a7928052732044b64fa61969cd8e37f3f7e3406f7201000000000000005237e4509f24bc9e8bf9bfd78b594219d86853ae16601f692f10c9b16b88f83b01000000000000006869737a2873891c0e4ef80a69e331f64ffc3663c9f3cdf86b440bb41827a252010000000000000098192016f83ef2fe74dba1ecc6526efc61b79f3177c00dc7c053160391858f700100000000000000a6f10752adf745ad55866031836194ad027ed8d6710d9853ef62982bcf45fd0e010000000000000078c35105ea4cd9d6ea0ba91b2c19e78f60931bef5918c1bd2e14259229e7c330010000000000000006d2846642c835b0fb1ff1ef908449376aa8e7c0f3352e55da9a1e97e9528b230100000000000000f8a681118ea0bb9dfde2813ce5144eeb5cee881c27883de1c0c759255ec326540100000000000000ac8b2b59f77eb5f3725961767878166a3817747018f18cf48c933e284320b90e0100000000000000e67c524e443cda4cd27dfcce70488a5d23a93c8c46a5e83ef5a69eff3a02fa5a0100000000000000a42289d68c963358f3c450a850242bb53121e73e17f0e2ea2b93327446bea33c0100000000000000caa53cf3e8ff5604d82bac45868bf3e25115f7e8f4d1cdfada68f8487d03e91b01000000000000000219b9856c2d50592c3821d7df9329795c9f6bc1e8d62b76072a4289617e5a060100000000000000e4ba5ffce05c90e6a5eea52b15922b6b3a78d13ca98f53fdbe08e5c24ce3c8410100000000000000a8e470f2339e3ed0da456780dd9207e9c6a02459238ec3057a6ade01fd6cd60a01000000000000005c7f05d77f853f3eb8d4989fce36d0808b6d2b881e26f13c01e939b5d489a57d01000000000000002addf696d5a42b0a3dada689731e00c991db936909df869ac6600662abc35310010000000000000072ed7192625403acef94aa9189f9f179deb89e60478816ca96c79b742f757e750100000000000000b4ffe3c5959ff37c11c6f19d91926cb97aa4bf0f604919e1753bb55664d061680100000000000000bc4c9b5425f9a2aa09e3239332c702dedbfffa2b9f4cc1b978b307fc1fa1337c0100000000000000e07d5b19d98a6f8ef5c964b507fcd88f500a2a0a9afd5cffe9d4e3f74d83b27f01000000000000001ee57665488b5ad6f7358157b73009a4ec4b14b4a03c976487e188f54bff7d640100000000000000be18787b5177e58664cd893fdaa20a3ca1bc9f1acbbb15036b046ae49c17c15b01000000000000004c50abd9279d48439dc8f93aaf679e47981f25372b63d838786dcaf7a3f8d0740100000000000000c49cfd152ce3ac1fa142b3b6e720cb173eb7f43c82f021418253a070036f870c01000000000000006ec27dfa7ac9d35eb002c55d3e4d56b4df928dd5c44323ac228305741c9e093601000000000000002803ef26679fce0495aa4066e053c4fa9cda5bfb54e24497fcb1bcef2902cb400100000000000000125becf853d8dc03bc97114ca0c751f6045bd7ef5580e58d32e3b47d7ac5b60301000000000000009237113cf97fee5fafbeb82de593f6125e9c498bd59f0369625f46b326c7d9170100000000000000722d7d57085c250af633f2d461874d59aebc8021eb1112ec8805db5ddea53e5a0100000000000000562fd7529c92f9370dd6d7ce60d06ea4a826cbefa238231993ecfc9e7725ca240100000000000000a20797f3a8669a909adb22a3aee2844dba82d231cc33f5d9be0e2c533cb82a6701000000000000006a1274b6748bf822ba80c9a9416abd3cf591b7aee6adabb8359b7dbed8ebb3770100000000000000dc1274e0689c7a3eac31b41ce352719529e65c01f26999f74ce17c9905fce87401000000000000009c9bc1074cf465c0dd39f74bfdf16a67f81339ba2ad117a0c7030b4f06787717010000000000000090708d1a49ef0acfea32147a109d08d9aa15ee975b5237d56ca9b8e324108d0f0100000000000000d8b38c77823eb1912bf8b743cca6c655ea1844ee468fbd78b394bf222bf8bc2001000000000000007251d6c376064f07f27d2532b05c091bd0978067b078c9f8d9984fd4216bdd4b0100000000000000487c6aad1cc26893dc7e498916a4c23d303f098c04b0a8b0c8242fbb7e73a16e0100000000000000d6152835cb59040a40a5a4ee3a4d481937b7ee9d4a5649abeac6c535d9e177080100000000000000bc4b95226de4e10f91e0254cfe4541232252deaa347f567df124aba81bac4d3f01000000000000001eb833e31087d6baffee4e7f1d9a8e3b7013ea392c81f74313a354ba82e92f6b01000000000000009c8c7c8c91df0ed53864f69017d530b5ebda3ab4bde3c67027e70893213d533a010000000000000018ec239eec5457ba11a41518e7c5f98266d1bdbd67602641d04706f95dba5c560100000000000000ee0c4369d7e5dca53a68ef223db7c858f2a9Line truncated
|
||||
"babeFinalizedBlockWeight": 5057865,
|
||||
"finalizedBlockHeader": "0x5a979217b503aa78e90ebcae20f6f220a4bc4ed6d776942fb850bcda517989e6a64bd4040e16472342dec8607f270f2d6ce99ceacb891aaddceaa3d7eace37129bca3338e2f6d0766dc5fcfc489eaa7a5d8284183494f34caa74847fed5c4fd8e05cbf290c0642414245b50103c9000000305803110000000098f2bd8c49c25f3a3ef98d184934741cd144aaf6d1ff9dd46479f449786a3d2941fb3a6b91133bd27200e607d83ce69787200ef74cf2708e876705e0657a9408debff3ab5bba7ae0c88832de6beee0f14de74abdf86218035889d763234fd00c04424545468403eb7277261742e27bca09f32dba2deb3ad262a859449d9bea27887340ba819a9205424142450101eaa77bb7093126995fc6905e2a89ee5d68de063bfae58eaaee9044deb42bef2372c3964800c13860b60bccf420d69bdbb6d1ea9cfdc58bfb9ea3ba0881236783",
|
||||
"grandpaAuthoritySet": "0xa5046f706506065685b322054d22e8a1f23ca9df75c32a88dda5214ad58b553b4cca01000000000000008a239af78d4659897af698b5670533fa6d215864be8c41e3a2fc4309f9f83dcb0100000000000000e2b9e72d9202e99526fa626d9a6651dfd7c1daec8fc6ba1130af96f7d21a42da010000000000000056b838dd2005e499be47ceef086df4ca9c5fbc1f81968391c31af062a8a8bef301000000000000004a3fc0fe1020c7f460c7bd23d0b657c03368552b2f0a12d80a1c406fd066e0db01000000000000009b992b5e9b99eb2cd3b2b648d6db81ed722a2feb4938d998b58dcb5d159fadfa0100000000000000a8b2ff688a9b1709f0371b49fe31d4d5a362d3fa11f64fcdc2941df9351edebe01000000000000003255f477781804204f24ddfd080346b0e75c7c5a92b37cec1fbbe79fa93407b001000000000000005a40b78008713bef1a35714bd32e0ec4c8905dd0728aeb4943280efe766936080100000000000000907fc1295e137738f8862d34490ad0746b80926e236e05ef8b958084f1d91a850100000000000000e8d7333e457fa740db924866152f31c3bfa6124c0367b0878a61aff2d5d6e27e0100000000000000426b2be4596a759a15028d84e08c5c56440e6cede68d088f47c671cc3387c3e6010000000000000070ba6e5985990ba1b1392713236f1b50df750f10f744cc6eb95fa7c5cfbc68760100000000000000e511d441d6d3c822cb276bb2a14b1c8071d9fe1995802383fb5dcf8a92edcfc2010000000000000003e385d1bbcaeed57930ff625035cd65781e3fe9de1d350d783ba261e678c0ac0100000000000000be2235b9d9c9164f494dd688000fb569a37d5c47912ef99b9ebda9318d13345e010000000000000049c0902ee37b569482da5474a15458b5dee16102fbc09b45878bb05f4a717acf01000000000000000e0ac68072ac35b8f19f98d13385b6eb75cf6fd4d513d9a2abfe9711b19883bb01000000000000007818639057900f8fb58e3aa8180f6108c251884a9fcb8041ca645cf612bc1eda0100000000000000a8dbfc6cb88ac105e25b9dc7f11e883631824647cd4d0c18014bb6239627b2900100000000000000ed7a7ea18c2d3b3effd9e8d7031592bc99517ca321c1faf7370f7d6d0fdebc47010000000000000041ef4a31eb7dc1e01f4630604e1908e644d7cdee3f66a60f98d6d59605326f8b0100000000000000890f5c296681b8c23038b3d36a491ca9e0e7e809e6455b2bcd28a13700f81abb010000000000000098df914cfd089eee8993fbfbd1522d6a31700c98bdc3a31cf69014a0ac123ecf010000000000000045ee96016a01c74778a6589fb2934ee9d83fd7a9e36509c4ef794d3d4f309bc60100000000000000f7e159462ec9dc0e54c6f6b8a83c4b551ee91613cad4208bbd563f16418328710100000000000000acd71d4269b3ba2a9de822d494c4d841708e1519c08de41d7fc6153eaf48da8101000000000000007c1024f3b93e54cb1cdde0d2f685e2e16cadabe59e407ebc45eabb07efebca6101000000000000000f42866aa6927f1c5916a8a1a5a5cba2d88e1fff957d80df701617d93ee6407801000000000000005cba2c6f569da1edc3778308da406f266aa53140381dbb3f14f22909ba6e30040100000000000000dac2e90de824b109043f70818d53eabe05e79d518bda504e951d7f4056b0b2720100000000000000652f455eb3d30486151c716a4031bc02bd00254ef3e8290fd29f946d958a46b60100000000000000589fc35b132ef9d7ed9a6787a203e5a3a37f8cd6c775f47ac5e589d55d381a64010000000000000044d68d3dbde0fbc3d667b7a004882caa9413b54785d820dc3654aff3a5b8e57d0100000000000000dccafad8bba5affbc80095564afc9376881907073a060979fabbb962bef08d56010000000000000078774f4f6130efaa55b5a06d1ef94ca420739dff79cbc24074bba841053e0aa20100000000000000aa3be8112cdf851411fe9e00fe9ef0a6b58217326ba7c1ab7ed9ce8a34620b250100000000000000c1c1ff5f66fefb5c0849e3b2333411d1f7e9dd4888600e334e7a33623f15fa360100000000000000f23b0362de32f952573d64c1f52ac4b8db0a7e15e7ad49a0c81d8f15b1f2ca8e0100000000000000594f51d31b1ae51a8c8ec48f5ab2428607440dd9c8f690ea377e99444529acb501000000000000002f40f2660559a4472aa7d2b5dac22c8925b9baeaec9fa2463cef2a77eb9d3a1b0100000000000000fa2c21f1a03638d4cb35202102240c2b85f5667924d5838cc503721117049da601000000000000008c461493dcd314f4b384169f3741954d8cde37bc8ad988149e40ddf18b4b95e101000000000000008aabd4f6d7ecec7a749c83fc7be33f3b4818cca6f93a4927b4964900df9454e50100000000000000fcc0d954c7d519851e0094cd9a015b4789022a37091e3a04791d0731bd85f0d301000000000000003948bfd09bbbc2fe6ac5dfe53146e32df50828eecf7b2b18cdcff8dc385cbd750100000000000000a2a4af2a11ae7ba812fcc32845583d0354a0f2ffb1271a53c9a976a0f47fff440100000000000000214ce2bd2912889eca3c69f45a3677989d66e20c3b26a52bbd378a4a71f5aa05010000000000000058e304af91397247f704ce95d60845629aa59a50fba1c13896bb2300708db47201000000000000008663371a5899d5e7a8bc99b9a6ce24c8c3f5f1149c1e70accf9560405c1a5bf201000000000000003d28789c6d574f3aa6d14360471901c5bf8255d072c2209fa22ac773bd32e5bf010000000000000099f574f1c57e766376a6976ceb3f44ea0420be7426d5c55b016a91e34091a0980100000000000000cc0c6a91c7971438b621cd9a3bd37a67f52ec4093336585d1e2c73416195e28a0100000000000000de52bfa88c4a76bb22fbebb0f078dfcc5a327034e0044e8f8034f2a96e7adfef0100000000000000bf3fd3d4065b306638662c3e789d830a63d9b343cef5f863d346058387844b58010000000000000005197187ac7eb8d0a3cfadd32f1a7fb662e7e4a16ea718af3f1e4437b865205b01000000000000007d09497b4d65162d28d4940f8eaf8342798e7794ec0f5028d163fea6156cb4f30100000000000000a5aa8fc244c0d6036f96ca0585214448d94a124157dad6a09b585d1638c2362d01000000000000007c077f9fbba618995d7df802a578934d36a5c0bec16dfcbaa7bce3879b4c44ce0100000000000000b98c920666586f392d129d480e78a965764ac82de45e691589b3aab7831a7f020100000000000000cb92ffeace78dbafe6fbf275741b4b38657fb81590712aa0bca7877931f6ad39010000000000000034a18d21097f479eecbe51b91c522a798582caafc787134bab6888854742406e010000000000000027791Line truncated
|
||||
"babeEpochChanges": "0x04cce759759d6a85a86c51aff0921c1c3eceb6533de064a8fae126e19eb051aee34ffd540101b375231100000000137f23110000000004dacfb39fbc4c04048c4495fa986149ba01c543bdd0232a2f55f63f7b69d669dea306550101137f231100000000738823110000000004098c43cf607c2c302f2c991322f99803e0e75476d7042d3027019f2cd43bca6afd0f5501017388231100000000d39123110000000000000c098c43cf607c2c302f2c991322f99803e0e75476d7042d3027019f2cd43bca6afd0f5501018d2400000000000073882311000000006009000000000000a50432904ec260963195582a1d339f9fbd2d5d76f551160105c922423cad0d94482f010000000000000076441c448aefcf40a72128edc9b44a4f7ef9c267c503e98f5f9c72d5e9e92f000100000000000000526e055ade4ac425e9b5f93ce4394601335d3599a5b742603126d78ce99a4a540100000000000000d0e8ddfdeacd8e163b01ca4da1a4e856312b0de412b492247070f2721f7d803801000000000000006c9c53f3824abe976dedc2792e1c6b8cc55c317eed2f30916c3229b60e5792060100000000000000ec8fcbae6ca9d402b51bb0bde913f0c29769d8d1b9653f4933db117890fe3872010000000000000046af93a15e552962706eb4779bf9dc24d49a6a55564250ca50e9a02c955b2a400100000000000000503cafb91c14eaa2931c4507090b27170b90a2a76d3cd4669f922202c781fc350100000000000000927ee7f0822e513b33a24900122754dee60a1898d90781dafdad6697fd2182070100000000000000ba395fcb33327501109ed40712c7a372d25bf3339edfabe2a1964fb7b5d4d82401000000000000003a36963a41de2036872df1c499f45acc0e7179457316ec2d99a6018e078f6a050100000000000000ce41346a050104a9d5d9226fba0f2bb54725836b440978730a878565a9198d0d01000000000000004c9886e2e62ee92a6778d6a7928052732044b64fa61969cd8e37f3f7e3406f720100000000000000c056f1879c1fd443e38244c26ef138218b3e43e70b1cb84bd7242e354cc9ee0e0100000000000000ca454487facfe1263c4ff3b1c5a83a4c832b9b6b8bc7c1accea4111b24276d1e0100000000000000c0698e756d794ed2451fe14a44153707bba5210aea0c3b2c027cd2f5c7edbf4001000000000000005237e4509f24bc9e8bf9bfd78b594219d86853ae16601f692f10c9b16b88f83b01000000000000001c38faa609249e0170c8474802a7e6907391fe0d550f0dfcb550249965898e030100000000000000a6f10752adf745ad55866031836194ad027ed8d6710d9853ef62982bcf45fd0e010000000000000078c35105ea4cd9d6ea0ba91b2c19e78f60931bef5918c1bd2e14259229e7c330010000000000000006d2846642c835b0fb1ff1ef908449376aa8e7c0f3352e55da9a1e97e9528b230100000000000000f8a681118ea0bb9dfde2813ce5144eeb5cee881c27883de1c0c759255ec3265401000000000000003a4a5b046c4b65c5a634c2ed909630384f543c66f59d0f4e9ece2e17e85b517001000000000000008a2ad071bfcfa2e03d48baa21d9e6118e8ac1192495edb1d84872041bee49004010000000000000064838c162c251bc34c0f5a2696183fb518527d4681821dbc3ad53b004a8da72201000000000000007e0c92cc60d7cb7282e0b1e8fea7ec2c4b751f672371373af0374e62f3b13b4a0100000000000000e67c524e443cda4cd27dfcce70488a5d23a93c8c46a5e83ef5a69eff3a02fa5a0100000000000000a42289d68c963358f3c450a850242bb53121e73e17f0e2ea2b93327446bea33c0100000000000000caa53cf3e8ff5604d82bac45868bf3e25115f7e8f4d1cdfada68f8487d03e91b0100000000000000a8e470f2339e3ed0da456780dd9207e9c6a02459238ec3057a6ade01fd6cd60a01000000000000005c7f05d77f853f3eb8d4989fce36d0808b6d2b881e26f13c01e939b5d489a57d0100000000000000506b05155f03f213d7516cf7ec50bd18a65187a8d10bd01501c1337232786f7601000000000000001abb26ba0076d5e001abdcf18d4454fe25c5076bc5ceb1b8f35d6041670c265f0100000000000000b4ffe3c5959ff37c11c6f19d91926cb97aa4bf0f604919e1753bb55664d061680100000000000000bc4c9b5425f9a2aa09e3239332c702dedbfffa2b9f4cc1b978b307fc1fa1337c01000000000000001ee57665488b5ad6f7358157b73009a4ec4b14b4a03c976487e188f54bff7d6401000000000000008610e1f00a1eba67d13225867c1f2972e60604e49bea7e5c1b10b29d4841b84a01000000000000007022686a613aa1387ea688fdd9a23170bf587f7f1dbaf7b8b022f120f3a4560d0100000000000000c47bd7af17b6ad4f01e2880542c8596f5a800a20eeebcb37756ffca94a6b9810010000000000000064d54109c3f4d89a9e36206a4c42e4ca0f74699c9e5f9df386fc154e6d51f43a010000000000000072a85a9dcc0af833b5a21a51ecbc4a9855c2bef8848e080ed70ac48cd1efdd5f0100000000000000c49cfd152ce3ac1fa142b3b6e720cb173eb7f43c82f021418253a070036f870c01000000000000000ee4b76ec5fabf011e00bc27b36039f3d397e021e8bcacf9532c90fc419fca0a01000000000000009237113cf97fee5fafbeb82de593f6125e9c498bd59f0369625f46b326c7d9170100000000000000722d7d57085c250af633f2d461874d59aebc8021eb1112ec8805db5ddea53e5a0100000000000000562fd7529c92f9370dd6d7ce60d06ea4a826cbefa238231993ecfc9e7725ca240100000000000000c654b48f2e2bc6c6e8fa8db811c69b8b9172aa475dc1c09c58ffd2aeab2eb92101000000000000008ef93e538ce16500f27b4c9a10ef7b02a46d8a9624778d0f83b9f7d8b560466501000000000000006a1274b6748bf822ba80c9a9416abd3cf591b7aee6adabb8359b7dbed8ebb3770100000000000000a893d2350e59c1937884d11aa7b680ceb7a49e7dba5a43c67bc7c20bf347c60c01000000000000003001ce583fa1f84555808f8d46c3242ce898060693aa0573f54d139263c1783901000000000000001c8c31172c865447aad1a964cdafae52f6e53c32d3ef2aec7f8956c9d1c7dd4d01000000000000007251d6c376064f07f27d2532b05c091bd0978067b078c9f8d9984fd4216bdd4b0100000000000000487c6aad1cc26893dc7e498916a4c23d303f098c04b0a8b0c8242fbb7e73a16e0100000000000000bc4b95226de4e10f91e0254cfe4541232252deaa347f567df124aba81bac4d3f01000000000000001eb833e31087d6baffee4e7f1d9a8e3b7013ea392c81f74313a354ba82e92f6b01000000000000009c8c7c8c91df0ed53864f69017d530b5ebdaLine truncated
|
||||
"babeFinalizedBlockWeight": 5580814,
|
||||
"finalizedBlockHeader": "0xf1b02b7b657e9f99bd45d90ceca5f0c12ea5593aecb67334fbabc68c7afd333e124754054ce4b693a31dcb3d9fcb30a76862851fcae57906d773e2df3319c901d9ba2e5fa6d2cb2595ea793e71e7ee64b60a5564591ae4e3b819ffd13200c8eb04945f790c0642414245b5010331000000dc80231100000000180847226dd6560905f43079ddc322bb037d4ab4a273abd3c7321307bf69dc482844dc9c4b832260566d22d46e285189f106e4e8e3a7944e49c0005d1bf18108a8f0e122316280fab15b337500367f76107180da6bca11d98aecc218aa5ba908044245454684032152341846f1d4fd5e13b9b1714841e365d920281a82187582c83a7e612d1226054241424501014271d1fbbddb0ead19be687875b8edf7430d5051c027204bf5ffe1cbd0f5f50f8156ea5ccacecea02657b6c303cf07ed886a5eb55388c4ac127fc31939d54f8e",
|
||||
"grandpaAuthoritySet": "0xa5046f706506065685b322054d22e8a1f23ca9df75c32a88dda5214ad58b553b4cca01000000000000008a239af78d4659897af698b5670533fa6d215864be8c41e3a2fc4309f9f83dcb0100000000000000e2b9e72d9202e99526fa626d9a6651dfd7c1daec8fc6ba1130af96f7d21a42da010000000000000056b838dd2005e499be47ceef086df4ca9c5fbc1f81968391c31af062a8a8bef30100000000000000e5914426faf919761a293345074f106393a9144420b3912e1dfe1f438975d21d0100000000000000938e7a90b6bb370ab1de25f714a6d4ec79766f0a83645df23e839b08800bc16e0100000000000000c52bd20e1e6b99f0bb13f67a2b5456040fc2cc06f8955257b5bbe98c92637ec80100000000000000ed70004b41d483626fb8080957192ded96e08afe38fd0d185ea02bbf3b2f6a7601000000000000002b76579e522d78199a07d5034b9b91696e831618597c428d75a42eaeaef58af00100000000000000e8d7333e457fa740db924866152f31c3bfa6124c0367b0878a61aff2d5d6e27e010000000000000084688f3dad92f60e8dc05840bb64c522cdd7b9330bd38ce8cbc1d19947a334ea01000000000000000882c3226125c75f50ded1a7c40761865f14e1aa38c26ec51239b31ea4605859010000000000000070ba6e5985990ba1b1392713236f1b50df750f10f744cc6eb95fa7c5cfbc68760100000000000000d343862beda4cf485b4a3b7e9b62d3b7e6263b47f65a7c88c88f6e0f1350c7a50100000000000000e910e45c162a4e9ae379bfcca70ac2165ce37f72652ef16898e86fd54f85ec110100000000000000bc08c197f7281aebdbd1dad6c2b02a6831ee8b5320745f9be57eb2a1f1b0fa9f0100000000000000e511d441d6d3c822cb276bb2a14b1c8071d9fe1995802383fb5dcf8a92edcfc20100000000000000f92d39f102331311ae6a4d051643cf10bd1490a5b00afaa4edc7d5d6af90f78b010000000000000049c0902ee37b569482da5474a15458b5dee16102fbc09b45878bb05f4a717acf01000000000000000e0ac68072ac35b8f19f98d13385b6eb75cf6fd4d513d9a2abfe9711b19883bb01000000000000007818639057900f8fb58e3aa8180f6108c251884a9fcb8041ca645cf612bc1eda0100000000000000a8dbfc6cb88ac105e25b9dc7f11e883631824647cd4d0c18014bb6239627b290010000000000000038ceeb0f56937fee55d1fe88c352d87a07941faa5c6539075cf7a328dc6f3bc80100000000000000c8579421534e4e52ba78c6ed533bf75fabfef7148b439a7585754a7cb0b892220100000000000000f7ca8ee474724b1be33094b4577237206eb96d6744d5d11220af11e6bd1bbe520100000000000000b966c337cbe37b05b4488b8b92e6f3938519bc97bebbed5f35de11ec28dab6cf010000000000000041ef4a31eb7dc1e01f4630604e1908e644d7cdee3f66a60f98d6d59605326f8b0100000000000000890f5c296681b8c23038b3d36a491ca9e0e7e809e6455b2bcd28a13700f81abb010000000000000098df914cfd089eee8993fbfbd1522d6a31700c98bdc3a31cf69014a0ac123ecf0100000000000000acd71d4269b3ba2a9de822d494c4d841708e1519c08de41d7fc6153eaf48da8101000000000000007c1024f3b93e54cb1cdde0d2f685e2e16cadabe59e407ebc45eabb07efebca610100000000000000fc0c1da84f15c7ae1216a9d1b932567618caf13e44bbb929c7d436396ee70ec001000000000000006a1c3443483a34e653ebc3d0857c00a3c8b48ff2c5e9549880ef47eacbc05c020100000000000000dac2e90de824b109043f70818d53eabe05e79d518bda504e951d7f4056b0b2720100000000000000652f455eb3d30486151c716a4031bc02bd00254ef3e8290fd29f946d958a46b6010000000000000044d68d3dbde0fbc3d667b7a004882caa9413b54785d820dc3654aff3a5b8e57d01000000000000007977808620ade75f3efeb26b0926229e0817c6163399f1d48f39ece8159c9ec201000000000000009fb61ecc2dc719b3efc8e1ac6e9c96edb4adbbfd9bab1ad2f8a013c5d8d5c48b0100000000000000409c49abb067e141ddd0a223f41a33956bb206dbcb1ce18a9b579232c11207a70100000000000000ae6e6876b6e4b821c67fd7dac6df59772f540dd76c7012c4fb9ef9d13ec863ec01000000000000003bf5b70cb5789c7ac4f6a4c97e6fabfe7a58fe9fed1d79f39c2aabc2c5816ece0100000000000000aa3be8112cdf851411fe9e00fe9ef0a6b58217326ba7c1ab7ed9ce8a34620b25010000000000000025a5b10abcc6e32a4a9f7daa0ad1d9a9ac72194e061bfbf3f064a8d3d8d688aa01000000000000002f40f2660559a4472aa7d2b5dac22c8925b9baeaec9fa2463cef2a77eb9d3a1b0100000000000000fa2c21f1a03638d4cb35202102240c2b85f5667924d5838cc503721117049da601000000000000008c461493dcd314f4b384169f3741954d8cde37bc8ad988149e40ddf18b4b95e10100000000000000f61e7fffc796acc7aafdc26c97d2add27faa8d9fcb4640fc60afbc6bc61ee19d01000000000000005d9af90688fd70834d26dbf19bc4f034e1ee3442c05c3603b798fe95a2aefb620100000000000000fcc0d954c7d519851e0094cd9a015b4789022a37091e3a04791d0731bd85f0d30100000000000000671af96bdaeb0dc9d8e1e61ed6de04ef1bf1bb86da5b8f392ea131bbda83bd31010000000000000076dae0ab6c60b0be802d6916f968b16b082e3d62ead357f5ac6e25ef98622d0f010000000000000062849cae8655a29c4a4c53ae3a5ddaf8d6cc53ccfe093ee97ca2f94ea85b570a01000000000000008663371a5899d5e7a8bc99b9a6ce24c8c3f5f1149c1e70accf9560405c1a5bf201000000000000003d28789c6d574f3aa6d14360471901c5bf8255d072c2209fa22ac773bd32e5bf0100000000000000cc0c6a91c7971438b621cd9a3bd37a67f52ec4093336585d1e2c73416195e28a0100000000000000de52bfa88c4a76bb22fbebb0f078dfcc5a327034e0044e8f8034f2a96e7adfef0100000000000000bf3fd3d4065b306638662c3e789d830a63d9b343cef5f863d346058387844b580100000000000000a5aa8fc244c0d6036f96ca0585214448d94a124157dad6a09b585d1638c2362d0100000000000000c488ef26295140d0ffa6b0df1cff9f8d8a1dc96d86fd199a389bcb2f37a87f340100000000000000b98c920666586f392d129d480e78a965764ac82de45e691589b3aab7831a7f020100000000000000cb92ffeace78dbafe6fbf275741b4b38657fb81590712aa0bca7877931f6ad39010000000000000034a18d21097f479eecbe51b91c522a798582caafc787134bab6888854742406e0100000000000000f7c9fLine truncated
|
||||
},
|
||||
"name": "Polkadot",
|
||||
"properties": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
+1
-1
@@ -40,7 +40,7 @@ scale-info = { workspace = true }
|
||||
scale-value = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport-native-tls", "http-client"] }
|
||||
jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport-tls", "http-client"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
scale-typegen-description = { workspace = true }
|
||||
heck = { workspace = true }
|
||||
|
||||
@@ -233,7 +233,7 @@ fn codegen(
|
||||
codegen.add_derives_for_type(ty, std::iter::once(derive), d.recursive);
|
||||
}
|
||||
|
||||
// Configure attribtues:
|
||||
// Configure attributes:
|
||||
let universal_attributes = raw_attributes
|
||||
.iter()
|
||||
.map(|raw| syn::parse_str(raw))
|
||||
|
||||
@@ -174,7 +174,7 @@ pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Re
|
||||
Usage:
|
||||
subxt explore pallet {pallet_placeholder}
|
||||
explore a specific pallet
|
||||
|
||||
|
||||
{pallets}
|
||||
"}?;
|
||||
return Ok(());
|
||||
@@ -327,8 +327,10 @@ pub mod tests {
|
||||
BeefyMmrApi
|
||||
BlockBuilder
|
||||
Core
|
||||
DryRunApi
|
||||
GenesisBuilder
|
||||
GrandpaApi
|
||||
LocationToAccountApi
|
||||
Metadata
|
||||
MmrApi
|
||||
OffchainWorkerApi
|
||||
@@ -336,6 +338,7 @@ pub mod tests {
|
||||
SessionKeys
|
||||
TaggedTransactionQueue
|
||||
TransactionPaymentApi
|
||||
XcmPaymentApi
|
||||
"};
|
||||
assert_eq!(output, expected_output);
|
||||
// if incorrect pallet, error:
|
||||
@@ -367,7 +370,7 @@ pub mod tests {
|
||||
Usage:
|
||||
subxt explore pallet Balances calls <CALL>
|
||||
explore a specific call of this pallet
|
||||
|
||||
|
||||
Available <CALL>'s in the \"Balances\" pallet:"};
|
||||
assert_eq_start!(output, start);
|
||||
let output = run_against_file("pallet Balances storage")
|
||||
@@ -378,7 +381,7 @@ pub mod tests {
|
||||
Usage:
|
||||
subxt explore pallet Balances storage <STORAGE_ENTRY>
|
||||
explore a specific storage entry of this pallet
|
||||
|
||||
|
||||
Available <STORAGE_ENTRY>'s in the \"Balances\" pallet:
|
||||
"};
|
||||
assert_eq_start!(output, start);
|
||||
@@ -390,7 +393,7 @@ pub mod tests {
|
||||
Usage:
|
||||
subxt explore pallet Balances constants <CONSTANT>
|
||||
explore a specific constant of this pallet
|
||||
|
||||
|
||||
Available <CONSTANT>'s in the \"Balances\" pallet:
|
||||
"};
|
||||
assert_eq_start!(output, start);
|
||||
@@ -402,7 +405,7 @@ pub mod tests {
|
||||
Usage:
|
||||
subxt explore pallet Balances events <EVENT>
|
||||
explore a specific event of this pallet
|
||||
|
||||
|
||||
Available <EVENT>'s in the \"Balances\" pallet:
|
||||
"};
|
||||
assert_eq_start!(output, start);
|
||||
@@ -427,11 +430,11 @@ pub mod tests {
|
||||
let start = formatdoc! {"
|
||||
Description:
|
||||
The `Metadata` api trait that returns metadata for the runtime.
|
||||
|
||||
|
||||
Usage:
|
||||
subxt explore api Metadata <METHOD>
|
||||
explore a specific runtime api method
|
||||
|
||||
|
||||
Available <METHOD>'s available for the \"Metadata\" runtime api:
|
||||
"};
|
||||
assert_eq_start!(output, start);
|
||||
|
||||
@@ -24,7 +24,7 @@ use subxt_metadata::RuntimeApiMetadata;
|
||||
/// None => Show pallet docs + available methods
|
||||
/// Some (invalid) => Show Error + available methods
|
||||
/// Some (valid) => Show method docs + output type description
|
||||
/// exectute is:
|
||||
/// execute is:
|
||||
/// false => Show input type description + Example Value
|
||||
/// true => validate (trailing args + build node connection)
|
||||
/// validation is:
|
||||
|
||||
+4
-1
@@ -25,7 +25,7 @@ quote = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
scale-info = { workspace = true }
|
||||
subxt-metadata = { workspace = true }
|
||||
jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport-native-tls", "http-client"], optional = true }
|
||||
jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport-tls", "http-client"], optional = true }
|
||||
hex = { workspace = true, features = ["std"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"], optional = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -43,3 +43,6 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.playground]
|
||||
defalt-features = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -262,7 +262,7 @@ impl RuntimeGenerator {
|
||||
/// The outer extrinsic enum.
|
||||
pub type Call = #call_path;
|
||||
|
||||
/// The outer error enum representing the DispatchError's Module variant.
|
||||
/// The outer error enum represents the DispatchError's Module variant.
|
||||
pub type Error = #error_path;
|
||||
|
||||
pub fn constants() -> ConstantsApi {
|
||||
|
||||
@@ -22,7 +22,7 @@ fn generate_runtime_api(
|
||||
type_gen: &TypeGenerator,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<(TokenStream2, TokenStream2), CodegenError> {
|
||||
// Trait name must remain as is (upper case) to identity the runtime call.
|
||||
// Trait name must remain as is (upper case) to identify the runtime call.
|
||||
let trait_name_str = api.name();
|
||||
// The snake case for the trait name.
|
||||
let trait_name_snake = format_ident!("{}", api.name().to_snake_case());
|
||||
|
||||
@@ -57,6 +57,9 @@ pub enum CodegenError {
|
||||
/// Cannot generate types.
|
||||
#[error("Type Generation failed: {0}")]
|
||||
TypeGeneration(#[from] TypegenError),
|
||||
/// Error when generating metadata from Wasm-runtime
|
||||
#[error("Failed to generate metadata from wasm file. reason: {0}")]
|
||||
Wasm(String),
|
||||
}
|
||||
|
||||
impl CodegenError {
|
||||
|
||||
+7
-2
@@ -342,10 +342,11 @@ fn default_derives(crate_path: &syn::Path) -> DerivesRegistry {
|
||||
parse_quote!(Debug),
|
||||
];
|
||||
|
||||
let attributes: [syn::Attribute; 3] = [
|
||||
let attributes: [syn::Attribute; 4] = [
|
||||
parse_quote!(#[encode_as_type(crate_path = #encode_crate_path)]),
|
||||
parse_quote!(#[decode_as_type(crate_path = #decode_crate_path)]),
|
||||
parse_quote!(#[codec(crate = #crate_path::ext::codec)]),
|
||||
parse_quote!(#[codec(dumb_trait_bound)]),
|
||||
];
|
||||
|
||||
let mut derives_registry = DerivesRegistry::new();
|
||||
@@ -357,7 +358,7 @@ fn default_derives(crate_path: &syn::Path) -> DerivesRegistry {
|
||||
fn default_substitutes(crate_path: &syn::Path) -> TypeSubstitutes {
|
||||
let mut type_substitutes = TypeSubstitutes::new();
|
||||
|
||||
let defaults: [(syn::Path, syn::Path); 12] = [
|
||||
let defaults: [(syn::Path, syn::Path); 13] = [
|
||||
(
|
||||
parse_quote!(bitvec::order::Lsb0),
|
||||
parse_quote!(#crate_path::utils::bits::Lsb0),
|
||||
@@ -370,6 +371,10 @@ fn default_substitutes(crate_path: &syn::Path) -> TypeSubstitutes {
|
||||
parse_quote!(sp_core::crypto::AccountId32),
|
||||
parse_quote!(#crate_path::utils::AccountId32),
|
||||
),
|
||||
(
|
||||
parse_quote!(fp_account::AccountId20),
|
||||
parse_quote!(#crate_path::utils::AccountId20),
|
||||
),
|
||||
(
|
||||
parse_quote!(sp_runtime::multiaddress::MultiAddress),
|
||||
parse_quote!(#crate_path::utils::MultiAddress),
|
||||
|
||||
+6
-2
@@ -35,7 +35,6 @@ std = [
|
||||
substrate-compat = ["sp-core", "sp-runtime"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
codec = { package = "parity-scale-codec", workspace = true, default-features = false, features = ["derive"] }
|
||||
scale-info = { workspace = true, default-features = false, features = ["bit-vec"] }
|
||||
scale-value = { workspace = true, default-features = false }
|
||||
@@ -45,7 +44,6 @@ scale-encode = { workspace = true, default-features = false, features = ["derive
|
||||
frame-metadata = { workspace = true, default-features = false }
|
||||
subxt-metadata = { workspace = true, default-features = false }
|
||||
derive-where = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
hex = { workspace = true, default-features = false, features = ["alloc"] }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
serde_json = { workspace = true, default-features = false, features = ["raw_value", "alloc"] }
|
||||
@@ -66,6 +64,9 @@ sp-core = { workspace = true, optional = true }
|
||||
sp-runtime = { workspace = true, optional = true }
|
||||
tracing = { workspace = true, default-features = false }
|
||||
|
||||
# AccountId20
|
||||
keccak-hash = { workspace = true}
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
bitvec = { workspace = true }
|
||||
@@ -84,3 +85,6 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.playground]
|
||||
defalt-features = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -3,15 +3,14 @@
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::blocks::extrinsic_signed_extensions::ExtrinsicSignedExtensions;
|
||||
use crate::utils::strip_compact_prefix;
|
||||
use crate::{
|
||||
config::Config,
|
||||
config::{Config, Hasher},
|
||||
error::{BlockError, Error, MetadataError},
|
||||
Metadata,
|
||||
};
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use codec::Decode;
|
||||
use codec::{Compact, CompactLen, Decode};
|
||||
use scale_decode::DecodeAsType;
|
||||
use subxt_metadata::PalletMetadata;
|
||||
|
||||
@@ -169,24 +168,28 @@ where
|
||||
const VERSION_MASK: u8 = 0b0111_1111;
|
||||
const LATEST_EXTRINSIC_VERSION: u8 = 4;
|
||||
|
||||
// removing the compact encoded prefix:
|
||||
let bytes: Arc<[u8]> = strip_compact_prefix(extrinsic_bytes)?.1.into();
|
||||
// Wrap all of the bytes in Arc for easy sharing.
|
||||
let bytes: Arc<[u8]> = Arc::from(extrinsic_bytes);
|
||||
|
||||
// The compact encoded length prefix.
|
||||
let prefix = <Compact<u64>>::decode(&mut &*extrinsic_bytes)?;
|
||||
let prefix_len = <Compact<u64>>::compact_len(&prefix.0);
|
||||
|
||||
// Extrinsic are encoded in memory in the following way:
|
||||
// - first byte: abbbbbbb (a = 0 for unsigned, 1 for signed, b = version)
|
||||
// - signature: [unknown TBD with metadata].
|
||||
// - extrinsic data
|
||||
let first_byte: u8 = Decode::decode(&mut &bytes[..])?;
|
||||
let version_byte: u8 = Decode::decode(&mut &bytes[prefix_len..])?;
|
||||
|
||||
let version = first_byte & VERSION_MASK;
|
||||
let version = version_byte & VERSION_MASK;
|
||||
if version != LATEST_EXTRINSIC_VERSION {
|
||||
return Err(BlockError::UnsupportedVersion(version).into());
|
||||
}
|
||||
|
||||
let is_signed = first_byte & SIGNATURE_MASK != 0;
|
||||
let is_signed = version_byte & SIGNATURE_MASK != 0;
|
||||
|
||||
// Skip over the first byte which denotes the version and signing.
|
||||
let cursor = &mut &bytes[1..];
|
||||
// Skip over the prefix and first byte which denotes the version and signing.
|
||||
let cursor = &mut &bytes[prefix_len + 1..];
|
||||
|
||||
let signed_details = is_signed
|
||||
.then(|| -> Result<SignedExtrinsicDetails, Error> {
|
||||
@@ -248,6 +251,12 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate and return the hash of the extrinsic, based on the configured hasher.
|
||||
pub fn hash(&self) -> T::Hash {
|
||||
// Use hash(), not hash_of(), because we don't want to double encode the bytes.
|
||||
T::Hasher::hash(&self.bytes)
|
||||
}
|
||||
|
||||
/// Is the extrinsic signed?
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.signed_details.is_some()
|
||||
@@ -630,6 +639,40 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tx_hashes_line_up() {
|
||||
let metadata = metadata();
|
||||
let ids = ExtrinsicPartTypeIds::new(&metadata).unwrap();
|
||||
|
||||
let tx = crate::dynamic::tx(
|
||||
"Test",
|
||||
"TestCall",
|
||||
vec![
|
||||
Value::u128(10),
|
||||
Value::bool(true),
|
||||
Value::string("SomeValue"),
|
||||
],
|
||||
);
|
||||
|
||||
// Encoded TX ready to submit.
|
||||
let tx_encoded = crate::tx::create_unsigned::<SubstrateConfig, _>(&tx, &metadata)
|
||||
.expect("Valid dynamic parameters are provided");
|
||||
|
||||
// Extrinsic details ready to decode.
|
||||
let extrinsic = ExtrinsicDetails::<SubstrateConfig>::decode_from(
|
||||
1,
|
||||
tx_encoded.encoded(),
|
||||
metadata,
|
||||
ids,
|
||||
)
|
||||
.expect("Valid extrinsic");
|
||||
|
||||
// Both of these types should produce the same bytes.
|
||||
assert_eq!(tx_encoded.encoded(), extrinsic.bytes(), "bytes should eq");
|
||||
// Both of these types should produce the same hash.
|
||||
assert_eq!(tx_encoded.hash(), extrinsic.hash(), "hashes should eq");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn statically_decode_extrinsic() {
|
||||
let metadata = metadata();
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
//!
|
||||
//! // Some extrinsics we'd like to decode:
|
||||
//! let ext_bytes = vec![
|
||||
//! hex::decode("280402000bf18367a38e01").unwrap(),
|
||||
//! hex::decode("c10184008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4801f4de97941fcc3f95c761cd58d480bb41ce64836850f51b6fcc7542e809eb0a346fe95eb1b72de542273d4f1b00b636eb025e2b0e98cc498a095e7ce48f3d4f82b501040000001848656c6c6f21").unwrap(),
|
||||
//! hex::decode("5102840090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe2201ac0c06f55cf3461067bbe48da16efbb50dfad555e2821ce20d37b2e42d6dcb439acd40f742b12ef00f8889944060b04373dc4d34a1992042fd269e8ec1e64a848502000004000090b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe2217000010632d5ec76b05").unwrap()
|
||||
//! hex::decode("1004020000").unwrap(),
|
||||
//! hex::decode("c10184001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c01a27c400241aeafdea1871b32f1f01e92acd272ddfe6b2f8b73b64c606572a530c470a94ef654f7baa5828474754a1fe31b59f91f6bb5c2cd5a07c22d4b8b8387350100000000001448656c6c6f").unwrap(),
|
||||
//! hex::decode("550284001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c0144bb92734447c893ab16d520fae0d455257550efa28ee66bf6dc942cb8b00d5d2799b98bc2865d21812278a9a266acd7352f40742ff11a6ce1f400013961598485010000000400008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a481700505a4f7e9f4eb106").unwrap()
|
||||
//! ];
|
||||
//!
|
||||
//! // Given some chain config and metadata, we know how to decode the bytes.
|
||||
|
||||
@@ -18,6 +18,7 @@ pub type DefaultExtrinsicParams<T> = signed_extensions::AnyOf<
|
||||
signed_extensions::CheckMortality<T>,
|
||||
signed_extensions::ChargeAssetTxPayment<T>,
|
||||
signed_extensions::ChargeTransactionPayment,
|
||||
signed_extensions::CheckMetadataHash,
|
||||
),
|
||||
>;
|
||||
|
||||
@@ -151,6 +152,7 @@ impl<T: Config> DefaultExtrinsicParamsBuilder<T> {
|
||||
check_mortality_params,
|
||||
charge_asset_tx_params,
|
||||
charge_transaction_params,
|
||||
(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use alloc::vec::Vec;
|
||||
/// This trait allows you to configure the "signed extra" and
|
||||
/// "additional" parameters that are a part of the transaction payload
|
||||
/// or the signer payload respectively.
|
||||
pub trait ExtrinsicParams<T: Config>: ExtrinsicParamsEncoder + Sized + 'static {
|
||||
pub trait ExtrinsicParams<T: Config>: ExtrinsicParamsEncoder + Sized + Send + 'static {
|
||||
/// These parameters can be provided to the constructor along with
|
||||
/// some default parameters that `subxt` understands, in order to
|
||||
/// help construct your [`ExtrinsicParams`] object.
|
||||
|
||||
@@ -58,7 +58,7 @@ pub trait Config: Sized + Send + Sync + 'static {
|
||||
type ExtrinsicParams: ExtrinsicParams<Self>;
|
||||
|
||||
/// This is used to identify an asset in the `ChargeAssetTxPayment` signed extension.
|
||||
type AssetId: Debug + Clone + Encode + DecodeAsType + EncodeAsType;
|
||||
type AssetId: Debug + Clone + Encode + DecodeAsType + EncodeAsType + Send;
|
||||
}
|
||||
|
||||
/// given some [`Config`], this return the other params needed for its `ExtrinsicParams`.
|
||||
|
||||
@@ -40,6 +40,61 @@ pub trait SignedExtension<T: Config>: ExtrinsicParams<T> {
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool;
|
||||
}
|
||||
|
||||
/// The [`CheckMetadataHash`] signed extension.
|
||||
pub struct CheckMetadataHash {
|
||||
// Eventually we might provide or calculate the metadata hash here,
|
||||
// but for now we never provide a hash and so this is empty.
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckMetadataHash {
|
||||
type Params = ();
|
||||
|
||||
fn new(_client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckMetadataHash {})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for CheckMetadataHash {
|
||||
fn encode_extra_to(&self, v: &mut Vec<u8>) {
|
||||
// A single 0 byte in the TX payload indicates that the chain should
|
||||
// _not_ expect any metadata hash to exist in the signer payload.
|
||||
0u8.encode_to(v);
|
||||
}
|
||||
fn encode_additional_to(&self, v: &mut Vec<u8>) {
|
||||
// We provide no metadata hash in the signer payload to align with the above.
|
||||
None::<()>.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> SignedExtension<T> for CheckMetadataHash {
|
||||
type Decoded = CheckMetadataHashMode;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckMetadataHash"
|
||||
}
|
||||
}
|
||||
|
||||
/// Is metadata checking enabled or disabled?
|
||||
// Dev note: The "Disabled" and "Enabled" variant names match those that the
|
||||
// signed extension will be encoded with, in order that DecodeAsType will work
|
||||
// properly.
|
||||
#[derive(Copy, Clone, Debug, DecodeAsType)]
|
||||
pub enum CheckMetadataHashMode {
|
||||
/// No hash was provided in the signer payload.
|
||||
Disabled,
|
||||
/// A hash was provided in the signer payload.
|
||||
Enabled,
|
||||
}
|
||||
|
||||
impl CheckMetadataHashMode {
|
||||
/// Is metadata checking enabled or disabled for this transaction?
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
match self {
|
||||
CheckMetadataHashMode::Disabled => false,
|
||||
CheckMetadataHashMode::Enabled => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckSpecVersion`] signed extension.
|
||||
pub struct CheckSpecVersion(u32);
|
||||
|
||||
@@ -380,7 +435,7 @@ impl<T: Config> SignedExtension<T> for ChargeTransactionPayment {
|
||||
/// ones are actually required for the chain in the correct order, ignoring the rest. This
|
||||
/// is a sensible default, and allows for a single configuration to work across multiple chains.
|
||||
pub struct AnyOf<T, Params> {
|
||||
params: Vec<Box<dyn ExtrinsicParamsEncoder>>,
|
||||
params: Vec<Box<dyn ExtrinsicParamsEncoder + Send + 'static>>,
|
||||
_marker: core::marker::PhantomData<(T, Params)>,
|
||||
}
|
||||
|
||||
@@ -415,7 +470,7 @@ macro_rules! impl_tuples {
|
||||
// Break and record as soon as we find a match:
|
||||
if $ident::matches(e.identifier(), e.extra_ty(), types) {
|
||||
let ext = $ident::new(client, params.$index)?;
|
||||
let boxed_ext: Box<dyn ExtrinsicParamsEncoder> = Box::new(ext);
|
||||
let boxed_ext: Box<dyn ExtrinsicParamsEncoder + Send + 'static> = Box::new(ext);
|
||||
exts_by_index.insert(idx, boxed_ext);
|
||||
break
|
||||
}
|
||||
|
||||
+125
-55
@@ -4,134 +4,168 @@
|
||||
|
||||
//! The errors that can be emitted in this crate.
|
||||
|
||||
use core::fmt::Display;
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use derive_more::{Display, From};
|
||||
use subxt_metadata::StorageHasher;
|
||||
|
||||
/// The error emitted when something goes wrong.
|
||||
#[derive(Debug, Display, From)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Codec error.
|
||||
#[display(fmt = "Scale codec error: {_0}")]
|
||||
Codec(codec::Error),
|
||||
/// Metadata error.
|
||||
#[display(fmt = "Metadata Error: {_0}")]
|
||||
Metadata(MetadataError),
|
||||
/// Storage address error.
|
||||
#[display(fmt = "Storage Error: {_0}")]
|
||||
StorageAddress(StorageAddressError),
|
||||
/// Error decoding to a [`crate::dynamic::Value`].
|
||||
#[display(fmt = "Error decoding into dynamic value: {_0}")]
|
||||
Decode(scale_decode::Error),
|
||||
/// Error encoding from a [`crate::dynamic::Value`].
|
||||
#[display(fmt = "Error encoding from dynamic value: {_0}")]
|
||||
Encode(scale_encode::Error),
|
||||
/// Error constructing the appropriate extrinsic params.
|
||||
#[display(fmt = "Extrinsic params error: {_0}")]
|
||||
ExtrinsicParams(ExtrinsicParamsError),
|
||||
/// Block body error.
|
||||
#[display(fmt = "Error working with block body: {_0}")]
|
||||
Block(BlockError),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Error::Codec(e) => write!(f, "Scale codec error: {e}"),
|
||||
Error::Metadata(e) => write!(f, "Metadata Error: {e}"),
|
||||
Error::StorageAddress(e) => write!(f, "Storage Error: {e}"),
|
||||
Error::Decode(e) => write!(f, "Error decoding into dynamic value: {e}"),
|
||||
Error::Encode(e) => write!(f, "Error encoding from dynamic value: {e}"),
|
||||
Error::ExtrinsicParams(e) => write!(f, "Extrinsic params error: {e}"),
|
||||
Error::Block(e) => write!(f, "Error working with block_body: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<scale_decode::visitor::DecodeError> for Error {
|
||||
fn from(value: scale_decode::visitor::DecodeError) -> Self {
|
||||
Error::Decode(value.into())
|
||||
}
|
||||
}
|
||||
impl_from!(ExtrinsicParamsError => Error::ExtrinsicParams);
|
||||
impl_from!(BlockError => Error::Block);
|
||||
impl_from!(MetadataError => Error::Metadata);
|
||||
impl_from!(scale_decode::Error => Error::Decode);
|
||||
impl_from!(scale_decode::visitor::DecodeError => Error::Decode);
|
||||
impl_from!(scale_encode::Error => Error::Encode);
|
||||
impl_from!(StorageAddressError => Error::StorageAddress);
|
||||
impl_from!(codec::Error => Error::Codec);
|
||||
|
||||
/// Block error
|
||||
#[derive(Clone, Debug, Display, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum BlockError {
|
||||
/// Extrinsic type ID cannot be resolved with the provided metadata.
|
||||
#[display(
|
||||
fmt = "Extrinsic type ID cannot be resolved with the provided metadata. Make sure this is a valid metadata"
|
||||
)]
|
||||
MissingType,
|
||||
/// Unsupported signature.
|
||||
#[display(fmt = "Unsupported extrinsic version, only version 4 is supported currently")]
|
||||
/// The extrinsic has an unsupported version.
|
||||
UnsupportedVersion(u8),
|
||||
/// Decoding error.
|
||||
#[display(fmt = "Cannot decode extrinsic: {_0}")]
|
||||
DecodingError(codec::Error),
|
||||
}
|
||||
|
||||
impl Display for BlockError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
BlockError::MissingType => write!(f, "Extrinsic type ID cannot be resolved with the provided metadata. Make sure this is a valid metadata"),
|
||||
BlockError::UnsupportedVersion(_) => write!(f, "Unsupported extrinsic version, only version 4 is supported currently"),
|
||||
BlockError::DecodingError(e) => write!(f, "Cannot decode extrinsic: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for BlockError {}
|
||||
|
||||
/// Something went wrong trying to access details in the metadata.
|
||||
#[derive(Clone, Debug, PartialEq, Display)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum MetadataError {
|
||||
/// The DispatchError type isn't available in the metadata
|
||||
#[display(fmt = "The DispatchError type isn't available")]
|
||||
DispatchErrorNotFound,
|
||||
/// Type not found in metadata.
|
||||
#[display(fmt = "Type with ID {_0} not found")]
|
||||
TypeNotFound(u32),
|
||||
/// Pallet not found (index).
|
||||
#[display(fmt = "Pallet with index {_0} not found")]
|
||||
PalletIndexNotFound(u8),
|
||||
/// Pallet not found (name).
|
||||
#[display(fmt = "Pallet with name {_0} not found")]
|
||||
PalletNameNotFound(String),
|
||||
/// Variant not found.
|
||||
#[display(fmt = "Variant with index {_0} not found")]
|
||||
VariantIndexNotFound(u8),
|
||||
/// Constant not found.
|
||||
#[display(fmt = "Constant with name {_0} not found")]
|
||||
ConstantNameNotFound(String),
|
||||
/// Call not found.
|
||||
#[display(fmt = "Call with name {_0} not found")]
|
||||
CallNameNotFound(String),
|
||||
/// Runtime trait not found.
|
||||
#[display(fmt = "Runtime trait with name {_0} not found")]
|
||||
RuntimeTraitNotFound(String),
|
||||
/// Runtime method not found.
|
||||
#[display(fmt = "Runtime method with name {_0} not found")]
|
||||
RuntimeMethodNotFound(String),
|
||||
/// Call type not found in metadata.
|
||||
#[display(fmt = "Call type not found in pallet with index {_0}")]
|
||||
CallTypeNotFoundInPallet(u8),
|
||||
/// Event type not found in metadata.
|
||||
#[display(fmt = "Event type not found in pallet with index {_0}")]
|
||||
EventTypeNotFoundInPallet(u8),
|
||||
/// Storage details not found in metadata.
|
||||
#[display(fmt = "Storage details not found in pallet with name {_0}")]
|
||||
StorageNotFoundInPallet(String),
|
||||
/// Storage entry not found.
|
||||
#[display(fmt = "Storage entry {_0} not found")]
|
||||
StorageEntryNotFound(String),
|
||||
/// The generated interface used is not compatible with the node.
|
||||
#[display(fmt = "The generated code is not compatible with the node")]
|
||||
IncompatibleCodegen,
|
||||
/// Custom value not found.
|
||||
#[display(fmt = "Custom value with name {_0} not found")]
|
||||
CustomValueNameNotFound(String),
|
||||
}
|
||||
impl Display for MetadataError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
MetadataError::DispatchErrorNotFound => {
|
||||
write!(f, "The DispatchError type isn't available")
|
||||
}
|
||||
MetadataError::TypeNotFound(e) => write!(f, "Type with ID {e} not found"),
|
||||
MetadataError::PalletIndexNotFound(e) => write!(f, "Pallet with index {e} not found"),
|
||||
MetadataError::PalletNameNotFound(e) => write!(f, "Pallet with name {e} not found"),
|
||||
MetadataError::VariantIndexNotFound(e) => write!(f, "Variant with index {e} not found"),
|
||||
MetadataError::ConstantNameNotFound(e) => write!(f, "Constant with name {e} not found"),
|
||||
MetadataError::CallNameNotFound(e) => write!(f, "Call with name {e} not found"),
|
||||
MetadataError::RuntimeTraitNotFound(e) => {
|
||||
write!(f, "Runtime trait with name {e} not found")
|
||||
}
|
||||
MetadataError::RuntimeMethodNotFound(e) => {
|
||||
write!(f, "Runtime method with name {e} not found")
|
||||
}
|
||||
MetadataError::CallTypeNotFoundInPallet(e) => {
|
||||
write!(f, "Call type not found in pallet with index {e}")
|
||||
}
|
||||
MetadataError::EventTypeNotFoundInPallet(e) => {
|
||||
write!(f, "Event type not found in pallet with index {e}")
|
||||
}
|
||||
MetadataError::StorageNotFoundInPallet(e) => {
|
||||
write!(f, "Storage details not found in pallet with name {e}")
|
||||
}
|
||||
MetadataError::StorageEntryNotFound(e) => write!(f, "Storage entry {e} not found"),
|
||||
MetadataError::IncompatibleCodegen => {
|
||||
write!(f, "The generated code is not compatible with the node")
|
||||
}
|
||||
MetadataError::CustomValueNameNotFound(e) => {
|
||||
write!(f, "Custom value with name {e} not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for MetadataError {}
|
||||
|
||||
/// Something went wrong trying to encode or decode a storage address.
|
||||
#[derive(Clone, Debug, Display)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum StorageAddressError {
|
||||
/// Storage lookup does not have the expected number of keys.
|
||||
#[display(fmt = "Storage lookup requires {expected} keys but more keys have been provided.")]
|
||||
TooManyKeys {
|
||||
/// The number of keys provided in the storage address.
|
||||
expected: usize,
|
||||
},
|
||||
/// This storage entry in the metadata does not have the correct number of hashers to fields.
|
||||
#[display(
|
||||
fmt = "Storage entry in metadata does not have the correct number of hashers to fields"
|
||||
)]
|
||||
WrongNumberOfHashers {
|
||||
/// The number of hashers in the metadata for this storage entry.
|
||||
hashers: usize,
|
||||
@@ -139,20 +173,12 @@ pub enum StorageAddressError {
|
||||
fields: usize,
|
||||
},
|
||||
/// We weren't given enough bytes to decode the storage address/key.
|
||||
#[display(fmt = "Not enough remaining bytes to decode the storage address/key")]
|
||||
NotEnoughBytes,
|
||||
/// We have leftover bytes after decoding the storage address.
|
||||
#[display(fmt = "We have leftover bytes after decoding the storage address")]
|
||||
TooManyBytes,
|
||||
/// The bytes of a storage address are not the expected address for decoding the storage keys of the address.
|
||||
#[display(
|
||||
fmt = "Storage address bytes are not the expected format. Addresses need to be at least 16 bytes (pallet ++ entry) and follow a structure given by the hashers defined in the metadata"
|
||||
)]
|
||||
UnexpectedAddressBytes,
|
||||
/// An invalid hasher was used to reconstruct a value from a chunk of bytes that is part of a storage address. Hashers where the hash does not contain the original value are invalid for this purpose.
|
||||
#[display(
|
||||
fmt = "An invalid hasher was used to reconstruct a value with type ID {ty_id} from a hash formed by a {hasher:?} hasher. This is only possible for concat-style hashers or the identity hasher"
|
||||
)]
|
||||
HasherCannotReconstructKey {
|
||||
/// Type id of the key's type.
|
||||
ty_id: u32,
|
||||
@@ -161,17 +187,47 @@ pub enum StorageAddressError {
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for StorageAddressError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
StorageAddressError::TooManyKeys { expected } => write!(
|
||||
f,
|
||||
"Storage lookup requires {expected} keys but more keys have been provided."
|
||||
),
|
||||
StorageAddressError::WrongNumberOfHashers { .. } => write!(
|
||||
f,
|
||||
"Storage entry in metadata does not have the correct number of hashers to fields"
|
||||
),
|
||||
StorageAddressError::NotEnoughBytes => write!(
|
||||
f,
|
||||
"Not enough remaining bytes to decode the storage address/key"
|
||||
),
|
||||
StorageAddressError::TooManyBytes => write!(
|
||||
f,
|
||||
"We have leftover bytes after decoding the storage address"
|
||||
),
|
||||
StorageAddressError::UnexpectedAddressBytes => write!(
|
||||
f,
|
||||
"Storage address bytes are not the expected format. Addresses need to be at least 16 bytes (pallet ++ entry) and follow a structure given by the hashers defined in the metadata"
|
||||
),
|
||||
StorageAddressError::HasherCannotReconstructKey { ty_id, hasher } => write!(
|
||||
f,
|
||||
"An invalid hasher was used to reconstruct a value with type ID {ty_id} from a hash formed by a {hasher:?} hasher. This is only possible for concat-style hashers or the identity hasher"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for StorageAddressError {}
|
||||
|
||||
/// An error that can be emitted when trying to construct an instance of [`crate::config::ExtrinsicParams`],
|
||||
/// encode data from the instance, or match on signed extensions.
|
||||
#[derive(Display, Debug)]
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ExtrinsicParamsError {
|
||||
/// Cannot find a type id in the metadata. The context provides some additional
|
||||
/// information about the source of the error (eg the signed extension name).
|
||||
#[display(fmt = "Cannot find type id '{type_id} in the metadata (context: {context})")]
|
||||
MissingTypeId {
|
||||
/// Type ID.
|
||||
type_id: u32,
|
||||
@@ -179,15 +235,29 @@ pub enum ExtrinsicParamsError {
|
||||
context: &'static str,
|
||||
},
|
||||
/// A signed extension in use on some chain was not provided.
|
||||
#[display(
|
||||
fmt = "The chain expects a signed extension with the name {_0}, but we did not provide one"
|
||||
)]
|
||||
UnknownSignedExtension(String),
|
||||
/// Some custom error.
|
||||
#[display(fmt = "Error constructing extrinsic parameters: {_0}")]
|
||||
Custom(Box<dyn CustomError>),
|
||||
}
|
||||
|
||||
impl Display for ExtrinsicParamsError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
ExtrinsicParamsError::MissingTypeId { type_id, context } => write!(
|
||||
f,
|
||||
"Cannot find type id '{type_id} in the metadata (context: {context})"
|
||||
),
|
||||
ExtrinsicParamsError::UnknownSignedExtension(e) => write!(
|
||||
f,
|
||||
"The chain expects a signed extension with the name {e}, but we did not provide one"
|
||||
),
|
||||
ExtrinsicParamsError::Custom(e) => {
|
||||
write!(f, "Error constructing extrinsic parameters: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Anything implementing this trait can be used in [`ExtrinsicParamsError::Custom`].
|
||||
#[cfg(feature = "std")]
|
||||
pub trait CustomError: std::error::Error + Send + Sync + 'static {}
|
||||
|
||||
@@ -18,4 +18,14 @@ macro_rules! cfg_substrate_compat {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($module_path:path => $delegate_ty:ident :: $variant:ident) => {
|
||||
impl From<$module_path> for $delegate_ty {
|
||||
fn from(val: $module_path) -> Self {
|
||||
$delegate_ty::$variant(val.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use {cfg_feature, cfg_substrate_compat};
|
||||
@@ -182,7 +182,7 @@ impl<K: codec::Encode + ?Sized> StaticStorageKey<K> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: codec::Decode + ?Sized> StaticStorageKey<K> {
|
||||
impl<K: codec::Decode> StaticStorageKey<K> {
|
||||
/// Decodes the encoded inner bytes into the type `K`.
|
||||
pub fn decoded(&self) -> Result<K, Error> {
|
||||
let decoded = K::decode(&mut self.bytes())?;
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::error::MetadataError;
|
||||
use crate::metadata::Metadata;
|
||||
use crate::Error;
|
||||
use alloc::borrow::{Cow, ToOwned};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
@@ -38,6 +39,32 @@ pub trait Payload {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! boxed_payload {
|
||||
($ty:path) => {
|
||||
impl<T: Payload + ?Sized> Payload for $ty {
|
||||
fn encode_call_data_to(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
self.as_ref().encode_call_data_to(metadata, out)
|
||||
}
|
||||
fn encode_call_data(&self, metadata: &Metadata) -> Result<Vec<u8>, Error> {
|
||||
self.as_ref().encode_call_data(metadata)
|
||||
}
|
||||
fn validation_details(&self) -> Option<ValidationDetails<'_>> {
|
||||
self.as_ref().validation_details()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
boxed_payload!(Box<T>);
|
||||
#[cfg(feature = "std")]
|
||||
boxed_payload!(std::sync::Arc<T>);
|
||||
#[cfg(feature = "std")]
|
||||
boxed_payload!(std::rc::Rc<T>);
|
||||
|
||||
/// Details required to validate the shape of a transaction payload against some metadata.
|
||||
pub struct ValidationDetails<'a> {
|
||||
/// The pallet name.
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
//! This doesn't contain much functionality itself, but is easy to convert to/from an `sp_core::AccountId32`
|
||||
//! for instance, to gain functionality without forcing a dependency on Substrate crates here.
|
||||
|
||||
use core::fmt::Display;
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A 32-byte cryptographic identifier. This is a simplified version of Substrate's
|
||||
@@ -105,19 +106,26 @@ impl AccountId32 {
|
||||
}
|
||||
|
||||
/// An error obtained from trying to interpret an SS58 encoded string into an AccountId32
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Display)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum FromSs58Error {
|
||||
#[display(fmt = "Base 58 requirement is violated")]
|
||||
BadBase58,
|
||||
#[display(fmt = "Length is bad")]
|
||||
BadLength,
|
||||
#[display(fmt = "Invalid checksum")]
|
||||
InvalidChecksum,
|
||||
#[display(fmt = "Invalid SS58 prefix byte.")]
|
||||
InvalidPrefix,
|
||||
}
|
||||
|
||||
impl Display for FromSs58Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
FromSs58Error::BadBase58 => write!(f, "Base 58 requirement is violated"),
|
||||
FromSs58Error::BadLength => write!(f, "Length is bad"),
|
||||
FromSs58Error::InvalidChecksum => write!(f, "Invalid checksum"),
|
||||
FromSs58Error::InvalidPrefix => write!(f, "Invalid SS58 prefix byte."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for FromSs58Error {}
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! `AccountId20` is a representation of Ethereum address derived from hashing the public key.
|
||||
|
||||
use core::fmt::Display;
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use codec::{Decode, Encode};
|
||||
use keccak_hash::keccak;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Encode,
|
||||
Decode,
|
||||
Debug,
|
||||
scale_encode::EncodeAsType,
|
||||
scale_decode::DecodeAsType,
|
||||
scale_info::TypeInfo,
|
||||
)]
|
||||
/// Ethereum-compatible `AccountId`.
|
||||
pub struct AccountId20(pub [u8; 20]);
|
||||
|
||||
impl AsRef<[u8]> for AccountId20 {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0[..]
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8; 20]> for AccountId20 {
|
||||
fn as_ref(&self) -> &[u8; 20] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 20]> for AccountId20 {
|
||||
fn from(x: [u8; 20]) -> Self {
|
||||
AccountId20(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountId20 {
|
||||
/// Convert to a public key hash
|
||||
pub fn checksum(&self) -> String {
|
||||
let hex_address = hex::encode(self.0);
|
||||
let hash = keccak(hex_address.as_bytes());
|
||||
|
||||
let mut checksum_address = String::with_capacity(42);
|
||||
checksum_address.push_str("0x");
|
||||
|
||||
for (i, ch) in hex_address.chars().enumerate() {
|
||||
// Get the corresponding nibble from the hash
|
||||
let nibble = hash[i / 2] >> (if i % 2 == 0 { 4 } else { 0 }) & 0xf;
|
||||
|
||||
if nibble >= 8 {
|
||||
checksum_address.push(ch.to_ascii_uppercase());
|
||||
} else {
|
||||
checksum_address.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
checksum_address
|
||||
}
|
||||
}
|
||||
|
||||
/// An error obtained from trying to interpret a hex encoded string into an AccountId20
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum FromChecksumError {
|
||||
BadLength,
|
||||
InvalidChecksum,
|
||||
InvalidPrefix,
|
||||
}
|
||||
|
||||
impl Display for FromChecksumError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
FromChecksumError::BadLength => write!(f, "Length is bad"),
|
||||
FromChecksumError::InvalidChecksum => write!(f, "Invalid checksum"),
|
||||
FromChecksumError::InvalidPrefix => write!(f, "Invalid checksum prefix byte."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for FromChecksumError {}
|
||||
|
||||
impl Serialize for AccountId20 {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.checksum())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AccountId20 {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
String::deserialize(deserializer)?
|
||||
.parse::<AccountId20>()
|
||||
.map_err(|e| serde::de::Error::custom(format!("{e:?}")))
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for AccountId20 {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "{}", self.checksum())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::str::FromStr for AccountId20 {
|
||||
type Err = FromChecksumError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.len() != 42 {
|
||||
return Err(FromChecksumError::BadLength);
|
||||
}
|
||||
if !s.starts_with("0x") {
|
||||
return Err(FromChecksumError::InvalidPrefix);
|
||||
}
|
||||
hex::decode(&s.as_bytes()[2..])
|
||||
.map_err(|_| FromChecksumError::InvalidChecksum)?
|
||||
.try_into()
|
||||
.map(AccountId20)
|
||||
.map_err(|_| FromChecksumError::BadLength)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deserialisation() {
|
||||
let key_hashes = vec![
|
||||
"0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac",
|
||||
"0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0",
|
||||
"0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc",
|
||||
"0x773539d4Ac0e786233D90A233654ccEE26a613D9",
|
||||
"0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB",
|
||||
"0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d",
|
||||
];
|
||||
|
||||
for key_hash in key_hashes {
|
||||
let parsed: AccountId20 = key_hash.parse().expect("Failed to parse");
|
||||
|
||||
let encoded = parsed.checksum();
|
||||
|
||||
// `encoded` should be equal to the initial key_hash
|
||||
assert_eq!(encoded, key_hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
//! Miscellaneous utility helpers.
|
||||
|
||||
mod account_id;
|
||||
mod account_id20;
|
||||
pub mod bits;
|
||||
mod era;
|
||||
mod multi_address;
|
||||
@@ -21,6 +22,7 @@ use codec::{Compact, Decode, Encode};
|
||||
use derive_where::derive_where;
|
||||
|
||||
pub use account_id::AccountId32;
|
||||
pub use account_id20::AccountId20;
|
||||
pub use era::Era;
|
||||
pub use multi_address::MultiAddress;
|
||||
pub use multi_signature::MultiSignature;
|
||||
|
||||
@@ -108,7 +108,7 @@ impl BackgroundTaskHandle {
|
||||
/// coming to/from Smoldot.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct BackgroundTask<TPlatform: PlatformRef, TChain> {
|
||||
channels: BackgroundTaskChannels,
|
||||
channels: BackgroundTaskChannels<TPlatform>,
|
||||
data: BackgroundTaskData<TPlatform, TChain>,
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ impl<TPlatform: PlatformRef, TChain> BackgroundTask<TPlatform, TChain> {
|
||||
pub(crate) fn new(
|
||||
client: SharedClient<TPlatform, TChain>,
|
||||
chain_id: smoldot_light::ChainId,
|
||||
from_back: smoldot_light::JsonRpcResponses,
|
||||
from_back: smoldot_light::JsonRpcResponses<TPlatform>,
|
||||
) -> (BackgroundTask<TPlatform, TChain>, BackgroundTaskHandle) {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
@@ -176,10 +176,11 @@ impl<TPlatform: PlatformRef, TChain> BackgroundTask<TPlatform, TChain> {
|
||||
tracing::trace!(target: LOG_TARGET, "Smoldot RPC responses channel closed");
|
||||
break;
|
||||
};
|
||||
|
||||
tracing::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Received smoldot RPC chain {:?} result {:?}",
|
||||
chain_id, back_message
|
||||
"Received smoldot RPC chain {chain_id:?} result {}",
|
||||
trim_message(&back_message),
|
||||
);
|
||||
|
||||
data.handle_rpc_response(back_message);
|
||||
@@ -191,11 +192,11 @@ impl<TPlatform: PlatformRef, TChain> BackgroundTask<TPlatform, TChain> {
|
||||
}
|
||||
}
|
||||
|
||||
struct BackgroundTaskChannels {
|
||||
struct BackgroundTaskChannels<TPlatform: PlatformRef> {
|
||||
/// Messages sent into this background task from the front end.
|
||||
from_front: UnboundedReceiverStream<Message>,
|
||||
/// Messages sent into the background task from Smoldot.
|
||||
from_back: smoldot_light::JsonRpcResponses,
|
||||
from_back: smoldot_light::JsonRpcResponses<TPlatform>,
|
||||
}
|
||||
|
||||
struct BackgroundTaskData<TPlatform: PlatformRef, TChain> {
|
||||
@@ -242,6 +243,18 @@ struct ActiveSubscription {
|
||||
unsubscribe_method: String,
|
||||
}
|
||||
|
||||
fn trim_message(s: &str) -> &str {
|
||||
const MAX_SIZE: usize = 512;
|
||||
if s.len() < MAX_SIZE {
|
||||
return s;
|
||||
}
|
||||
|
||||
match s.char_indices().nth(MAX_SIZE) {
|
||||
None => s,
|
||||
Some((idx, _)) => &s[..idx],
|
||||
}
|
||||
}
|
||||
|
||||
impl<TPlatform: PlatformRef, TChain> BackgroundTaskData<TPlatform, TChain> {
|
||||
/// Fetch and increment the request ID.
|
||||
fn next_id(&mut self) -> usize {
|
||||
@@ -359,7 +372,7 @@ impl<TPlatform: PlatformRef, TChain> BackgroundTaskData<TPlatform, TChain> {
|
||||
/// Parse the response received from the light client and sent it to the appropriate user.
|
||||
fn handle_rpc_response(&mut self, response: String) {
|
||||
let chain_id = self.chain_id;
|
||||
tracing::trace!(target: LOG_TARGET, "Received from smoldot response='{response}' chain={chain_id:?}");
|
||||
tracing::trace!(target: LOG_TARGET, "Received from smoldot response='{}' chain={chain_id:?}", trim_message(&response));
|
||||
|
||||
match RpcResponse::from_str(&response) {
|
||||
Ok(RpcResponse::Method { id, result }) => {
|
||||
|
||||
@@ -185,7 +185,7 @@ impl LightClientRpc {
|
||||
pub(crate) fn new_raw<TPlat, TChain>(
|
||||
client: impl Into<SharedClient<TPlat, TChain>>,
|
||||
chain_id: smoldot_light::ChainId,
|
||||
rpc_responses: smoldot_light::JsonRpcResponses,
|
||||
rpc_responses: smoldot_light::JsonRpcResponses<TPlat>,
|
||||
) -> Self
|
||||
where
|
||||
TPlat: smoldot_light::platform::PlatformRef + Send + 'static,
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
|
||||
use super::wasm_socket::WasmSocket;
|
||||
|
||||
use core::time::Duration;
|
||||
use core::{
|
||||
fmt::{self, Write as _},
|
||||
net::IpAddr,
|
||||
time::Duration,
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use smoldot::libp2p::with_buffers;
|
||||
use smoldot_light::platform::{
|
||||
Address, ConnectionType, IpAddr, MultiStreamAddress, MultiStreamWebRtcConnection, PlatformRef,
|
||||
SubstreamDirection,
|
||||
Address, ConnectionType, LogLevel, MultiStreamAddress, MultiStreamWebRtcConnection,
|
||||
PlatformRef, SubstreamDirection,
|
||||
};
|
||||
|
||||
use std::{io, net::SocketAddr, pin::Pin};
|
||||
@@ -187,4 +191,33 @@ impl PlatformRef for SubxtPlatform {
|
||||
super::wasm_helpers::sleep(duration).await;
|
||||
}))
|
||||
}
|
||||
|
||||
fn log<'a>(
|
||||
&self,
|
||||
log_level: LogLevel,
|
||||
log_target: &'a str,
|
||||
message: &'a str,
|
||||
key_values: impl Iterator<Item = (&'a str, &'a dyn fmt::Display)>,
|
||||
) {
|
||||
let mut message_build = String::with_capacity(128);
|
||||
message_build.push_str(message);
|
||||
let mut first = true;
|
||||
for (key, value) in key_values {
|
||||
if first {
|
||||
let _ = write!(message_build, "; ");
|
||||
first = false;
|
||||
} else {
|
||||
let _ = write!(message_build, ", ");
|
||||
}
|
||||
let _ = write!(message_build, "{}={}", key, value);
|
||||
}
|
||||
|
||||
match log_level {
|
||||
LogLevel::Error => tracing::error!("target={} {}", log_target, message_build),
|
||||
LogLevel::Warn => tracing::warn!("target={} {}", log_target, message_build),
|
||||
LogLevel::Info => tracing::info!("target={} {}", log_target, message_build),
|
||||
LogLevel::Debug => tracing::debug!("target={} {}", log_target, message_build),
|
||||
LogLevel::Trace => tracing::trace!("target={} {}", log_target, message_build),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ impl<TPlat: sl::platform::PlatformRef, TChain> SharedClient<TPlat, TChain> {
|
||||
pub(crate) fn add_chain(
|
||||
&self,
|
||||
config: sl::AddChainConfig<'_, TChain, impl Iterator<Item = sl::ChainId>>,
|
||||
) -> Result<sl::AddChainSuccess, sl::AddChainError> {
|
||||
) -> Result<sl::AddChainSuccess<TPlat>, sl::AddChainError> {
|
||||
self.client
|
||||
.lock()
|
||||
.expect("mutex should not be poisoned")
|
||||
|
||||
+10
-1
@@ -15,6 +15,7 @@ description = "Generate types and helpers for interacting with Substrate runtime
|
||||
|
||||
[features]
|
||||
web = ["subxt-codegen/web"]
|
||||
runtime-path = ["sp-io", "sc-executor-common", "sp-state-machine", "sp-maybe-compressed-blob", "sc-executor"]
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -22,8 +23,16 @@ proc-macro = true
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", workspace = true }
|
||||
darling = { workspace = true }
|
||||
proc-macro-error = { workspace = true }
|
||||
proc-macro-error2 = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
subxt-codegen = { workspace = true, features = ["fetch-metadata"] }
|
||||
scale-typegen = { workspace = true }
|
||||
sc-executor = { workspace = true, optional = true }
|
||||
sp-maybe-compressed-blob = { workspace = true, optional = true }
|
||||
sp-state-machine = { workspace = true, optional = true }
|
||||
sp-io = { workspace = true, optional = true }
|
||||
sc-executor-common = { workspace = true, optional = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
+42
-5
@@ -3,13 +3,13 @@
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Subxt macro for generating Substrate runtime interfaces.
|
||||
|
||||
extern crate proc_macro;
|
||||
// TODO: The workspace lint is not working properly so it's disabled here for now.
|
||||
#![allow(clippy::manual_unwrap_or_default)]
|
||||
|
||||
use codec::Decode;
|
||||
use darling::{ast::NestedMeta, FromMeta};
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_error::{abort_call_site, proc_macro_error};
|
||||
use proc_macro_error2::{abort_call_site, proc_macro_error};
|
||||
use quote::ToTokens;
|
||||
use scale_typegen::typegen::{
|
||||
settings::substitutes::path_segments,
|
||||
@@ -23,6 +23,9 @@ use subxt_codegen::{
|
||||
};
|
||||
use syn::{parse_macro_input, punctuated::Punctuated};
|
||||
|
||||
#[cfg(feature = "runtime-path")]
|
||||
mod wasm_loader;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct OuterAttribute(syn::Attribute);
|
||||
|
||||
@@ -60,6 +63,9 @@ struct RuntimeMetadataArgs {
|
||||
no_default_substitutions: bool,
|
||||
#[darling(default)]
|
||||
unstable_metadata: darling::util::Flag,
|
||||
#[cfg(feature = "runtime-path")]
|
||||
#[darling(default)]
|
||||
runtime_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromMeta)]
|
||||
@@ -85,6 +91,7 @@ struct SubstituteType {
|
||||
}
|
||||
|
||||
// Note: docs for this are in the subxt library; don't add further docs here as they will be appended.
|
||||
#[allow(missing_docs)]
|
||||
#[proc_macro_attribute]
|
||||
#[proc_macro_error]
|
||||
pub fn subxt(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
@@ -205,6 +212,22 @@ fn validate_type_path(path: &syn::Path, metadata: &Metadata) {
|
||||
fn fetch_metadata(args: &RuntimeMetadataArgs) -> Result<subxt_codegen::Metadata, TokenStream> {
|
||||
// Do we want to fetch unstable metadata? This only works if fetching from a URL.
|
||||
let unstable_metadata = args.unstable_metadata.is_present();
|
||||
|
||||
#[cfg(feature = "runtime-path")]
|
||||
if let Some(path) = &args.runtime_path {
|
||||
if args.runtime_metadata_insecure_url.is_some() || args.runtime_metadata_path.is_some() {
|
||||
abort_call_site!(
|
||||
"Only one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' or `runtime_path` must be provided"
|
||||
);
|
||||
};
|
||||
let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
|
||||
let root_path = std::path::Path::new(&root);
|
||||
let path = root_path.join(path);
|
||||
|
||||
let metadata = wasm_loader::from_wasm_file(&path).map_err(|e| e.into_compile_error())?;
|
||||
return Ok(metadata);
|
||||
};
|
||||
|
||||
let metadata = match (
|
||||
&args.runtime_metadata_path,
|
||||
&args.runtime_metadata_insecure_url,
|
||||
@@ -238,12 +261,26 @@ fn fetch_metadata(args: &RuntimeMetadataArgs) -> Result<subxt_codegen::Metadata,
|
||||
.and_then(|b| subxt_codegen::Metadata::decode(&mut &*b).map_err(Into::into))
|
||||
.map_err(|e| e.into_compile_error())?
|
||||
}
|
||||
#[cfg(feature = "runtime-path")]
|
||||
(None, None) => {
|
||||
abort_call_site!(
|
||||
"One of 'runtime_metadata_path' or 'runtime_metadata_insecure_url' must be provided"
|
||||
"At least one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' or 'runtime_path` can be provided"
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "runtime-path"))]
|
||||
(None, None) => {
|
||||
abort_call_site!(
|
||||
"At least one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' can be provided"
|
||||
)
|
||||
}
|
||||
(Some(_), Some(_)) => {
|
||||
#[cfg(feature = "runtime-path")]
|
||||
_ => {
|
||||
abort_call_site!(
|
||||
"Only one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' or 'runtime_path` can be provided"
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "runtime-path"))]
|
||||
_ => {
|
||||
abort_call_site!(
|
||||
"Only one of 'runtime_metadata_path' or 'runtime_metadata_insecure_url' can be provided"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
use codec::Decode;
|
||||
use sc_executor::{WasmExecutionMethod, WasmExecutor};
|
||||
use sc_executor_common::runtime_blob::RuntimeBlob;
|
||||
use sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT;
|
||||
use subxt_codegen::{fetch_metadata::fetch_metadata_from_file_blocking, CodegenError, Metadata};
|
||||
|
||||
/// Result type shorthand
|
||||
pub type WasmMetadataResult<A> = Result<A, CodegenError>;
|
||||
|
||||
/// Uses wasm artifact produced by compiling the runtime to generate metadata
|
||||
pub fn from_wasm_file(wasm_file_path: &Path) -> WasmMetadataResult<Metadata> {
|
||||
let wasm_file = fetch_metadata_from_file_blocking(wasm_file_path)
|
||||
.map_err(Into::<CodegenError>::into)
|
||||
.and_then(maybe_decompress)?;
|
||||
call_and_decode(wasm_file)
|
||||
}
|
||||
|
||||
fn call_and_decode(wasm_file: Vec<u8>) -> WasmMetadataResult<Metadata> {
|
||||
let mut ext: sp_state_machine::BasicExternalities = Default::default();
|
||||
|
||||
let executor: WasmExecutor<sp_io::SubstrateHostFunctions> = WasmExecutor::builder()
|
||||
.with_execution_method(WasmExecutionMethod::default())
|
||||
.with_offchain_heap_alloc_strategy(sc_executor::HeapAllocStrategy::Dynamic {
|
||||
maximum_pages: Some(64),
|
||||
})
|
||||
.with_max_runtime_instances(1)
|
||||
.with_runtime_cache_size(1)
|
||||
.build();
|
||||
|
||||
let runtime_blob =
|
||||
RuntimeBlob::new(&wasm_file).map_err(|e| CodegenError::Wasm(e.to_string()))?;
|
||||
let metadata_encoded = executor
|
||||
.uncached_call(runtime_blob, &mut ext, true, "Metadata_metadata", &[])
|
||||
.map_err(|_| CodegenError::Wasm("method \"Metadata_metadata\" doesnt exist".to_owned()))?;
|
||||
|
||||
let metadata = <Vec<u8>>::decode(&mut &metadata_encoded[..]).map_err(CodegenError::Decode)?;
|
||||
|
||||
decode(metadata)
|
||||
}
|
||||
|
||||
fn decode(encoded_metadata: Vec<u8>) -> WasmMetadataResult<Metadata> {
|
||||
Metadata::decode(&mut encoded_metadata.as_ref()).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn maybe_decompress(file_contents: Vec<u8>) -> WasmMetadataResult<Vec<u8>> {
|
||||
sp_maybe_compressed_blob::decompress(file_contents.as_ref(), CODE_BLOB_BOMB_LIMIT)
|
||||
.map_err(|e| CodegenError::Wasm(e.to_string()))
|
||||
.map(Cow::into_owned)
|
||||
}
|
||||
+3
-1
@@ -23,7 +23,6 @@ frame-metadata = { workspace = true, default-features = false, features = ["curr
|
||||
codec = { package = "parity-scale-codec", workspace = true, default-features = false, features = ["derive"] }
|
||||
sp-crypto-hashing = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
bitvec = { workspace = true, features = ["alloc"] }
|
||||
@@ -38,3 +37,6 @@ bench = false
|
||||
[[bench]]
|
||||
name = "bench"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -3,7 +3,7 @@
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Benchmarks for metadata hashing.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
use codec::Decode;
|
||||
use criterion::*;
|
||||
use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed};
|
||||
@@ -92,4 +92,5 @@ criterion_group!(
|
||||
bench_get_constant_hash,
|
||||
bench_get_storage_hash,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
@@ -2,34 +2,53 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use core::fmt::Display;
|
||||
|
||||
use alloc::string::String;
|
||||
use derive_more::Display;
|
||||
|
||||
mod v14;
|
||||
mod v15;
|
||||
|
||||
/// An error emitted if something goes wrong converting [`frame_metadata`]
|
||||
/// types into [`crate::Metadata`].
|
||||
#[derive(Debug, Display, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum TryFromError {
|
||||
/// Type missing from type registry
|
||||
#[display(fmt = "Type id {_0} is expected but not found in the type registry")]
|
||||
TypeNotFound(u32),
|
||||
/// Type was not a variant/enum type
|
||||
#[display(fmt = "Type {_0} was not a variant/enum type, but is expected to be one")]
|
||||
VariantExpected(u32),
|
||||
/// An unsupported metadata version was provided.
|
||||
#[display(fmt = "Cannot convert v{_0} metadata into Metadata type")]
|
||||
UnsupportedMetadataVersion(u32),
|
||||
/// Type name missing from type registry
|
||||
#[display(fmt = "Type name {_0} is expected but not found in the type registry")]
|
||||
TypeNameNotFound(String),
|
||||
/// Invalid type path.
|
||||
#[display(fmt = "Type has an invalid path {_0}")]
|
||||
InvalidTypePath(String),
|
||||
}
|
||||
|
||||
impl Display for TryFromError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
TryFromError::TypeNotFound(e) => write!(
|
||||
f,
|
||||
"Type id {e} is expected but not found in the type registry"
|
||||
),
|
||||
TryFromError::VariantExpected(e) => write!(
|
||||
f,
|
||||
"Type {e} was not a variant/enum type, but is expected to be one"
|
||||
),
|
||||
TryFromError::UnsupportedMetadataVersion(e) => {
|
||||
write!(f, "Cannot convert v{e} metadata into Metadata type")
|
||||
}
|
||||
TryFromError::TypeNameNotFound(e) => write!(
|
||||
f,
|
||||
"Type name {e} is expected but not found in the type registry"
|
||||
),
|
||||
TryFromError::InvalidTypePath(e) => write!(f, "Type has an invalid path {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for TryFromError {}
|
||||
|
||||
|
||||
@@ -344,7 +344,7 @@ fn generate_outer_enums(
|
||||
let Some(last) = call_path.last_mut() else {
|
||||
return Err(TryFromError::InvalidTypePath("RuntimeCall".into()));
|
||||
};
|
||||
*last = "RuntimeError".to_owned();
|
||||
"RuntimeError".clone_into(last);
|
||||
generate_outer_error_enum_type(metadata, call_path)
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -441,7 +441,7 @@ impl StorageHasher {
|
||||
///
|
||||
/// 1. A fixed size hash. (not present for [`StorageHasher::Identity`]).
|
||||
/// 2. The SCALE encoded key that was used as an input to the hasher (only present for
|
||||
/// [`StorageHasher::Twox64Concat`], [`StorageHasher::Blake2_128Concat`] or [`StorageHasher::Identity`]).
|
||||
/// [`StorageHasher::Twox64Concat`], [`StorageHasher::Blake2_128Concat`] or [`StorageHasher::Identity`]).
|
||||
///
|
||||
/// This function returns the number of bytes used to represent the first of these.
|
||||
pub fn len_excluding_key(&self) -> usize {
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ for CRATE_DIR in ${ORDER[@]}; do
|
||||
sleep 3
|
||||
remote_version
|
||||
if [ "$REMOTE_VERSION" = "$VERSION" ]; then
|
||||
echo "🥳 $NAME@$VERSION published succesfully."
|
||||
echo "🥳 $NAME@$VERSION published successfully."
|
||||
sleep 3
|
||||
break
|
||||
else
|
||||
|
||||
+27
-6
@@ -23,9 +23,13 @@ std = [
|
||||
"sha2/std",
|
||||
"hmac/std",
|
||||
"bip39/std",
|
||||
"schnorrkel/std",
|
||||
"secp256k1/std",
|
||||
"sp-core/std"
|
||||
"schnorrkel?/std",
|
||||
"secp256k1?/std",
|
||||
"serde?/std",
|
||||
"serde_json?/std",
|
||||
"base64?/std",
|
||||
"scrypt?/std",
|
||||
"crypto_secretbox?/std",
|
||||
]
|
||||
|
||||
# Pick the signer implementation(s) you need by enabling the
|
||||
@@ -36,6 +40,9 @@ sr25519 = ["schnorrkel"]
|
||||
ecdsa = ["secp256k1"]
|
||||
unstable-eth = ["keccak-hash", "ecdsa", "secp256k1", "bip32"]
|
||||
|
||||
# Enable support for loading key pairs from polkadot-js json.
|
||||
polkadot-js-compat = ["std", "subxt", "sr25519", "base64", "scrypt", "crypto_secretbox", "serde", "serde_json"]
|
||||
|
||||
# Make the keypair algorithms here compatible with Subxt's Signer trait,
|
||||
# so that they can be used to sign transactions for compatible chains.
|
||||
subxt = ["dep:subxt-core"]
|
||||
@@ -50,9 +57,10 @@ secrecy = { workspace = true }
|
||||
regex = { workspace = true, features = ["unicode"] }
|
||||
hex = { workspace = true, features = ["alloc"] }
|
||||
cfg-if = { workspace = true }
|
||||
codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] }
|
||||
codec = { package = "parity-scale-codec", workspace = true, features = [
|
||||
"derive",
|
||||
] }
|
||||
sp-crypto-hashing = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
pbkdf2 = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
@@ -60,9 +68,19 @@ zeroize = { workspace = true }
|
||||
bip39 = { workspace = true }
|
||||
bip32 = { workspace = true, features = ["alloc", "secp256k1"], optional = true }
|
||||
schnorrkel = { workspace = true, optional = true }
|
||||
secp256k1 = { workspace = true, optional = true, features = ["alloc", "recovery"] }
|
||||
secp256k1 = { workspace = true, optional = true, features = [
|
||||
"alloc",
|
||||
"recovery",
|
||||
] }
|
||||
keccak-hash = { workspace = true, optional = true }
|
||||
|
||||
# These are used if the polkadot-js-compat feature is enabled
|
||||
serde = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
base64 = { workspace = true, optional = true, features = ["alloc"] }
|
||||
scrypt = { workspace = true, default-features = false, optional = true }
|
||||
crypto_secretbox = { workspace = true, optional = true, features = ["alloc", "salsa20"] }
|
||||
|
||||
# We only pull this in to enable the JS flag for schnorrkel to use.
|
||||
getrandom = { workspace = true, optional = true }
|
||||
|
||||
@@ -81,3 +99,6 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.playground]
|
||||
defalt-features = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -2,9 +2,10 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use core::fmt::Display;
|
||||
|
||||
use super::DeriveJunction;
|
||||
use alloc::vec::Vec;
|
||||
use derive_more::Display;
|
||||
use regex::Regex;
|
||||
use secrecy::SecretString;
|
||||
|
||||
@@ -117,13 +118,20 @@ impl core::str::FromStr for SecretUri {
|
||||
}
|
||||
|
||||
/// This is returned if `FromStr` cannot parse a string into a `SecretUri`.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Display)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum SecretUriError {
|
||||
/// Parsing the secret URI from a string failed; wrong format.
|
||||
#[display(fmt = "Invalid secret phrase format")]
|
||||
InvalidFormat,
|
||||
}
|
||||
|
||||
impl Display for SecretUriError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
SecretUriError::InvalidFormat => write!(f, "Invalid secret phrase format"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for SecretUriError {}
|
||||
|
||||
|
||||
+26
-11
@@ -6,8 +6,7 @@
|
||||
use codec::Encode;
|
||||
|
||||
use crate::crypto::{seed_from_entropy, DeriveJunction, SecretUri};
|
||||
use core::str::FromStr;
|
||||
use derive_more::{Display, From};
|
||||
use core::{fmt::Display, str::FromStr};
|
||||
use hex::FromHex;
|
||||
use secp256k1::{ecdsa::RecoverableSignature, Message, Secp256k1, SecretKey};
|
||||
use secrecy::ExposeSecret;
|
||||
@@ -158,10 +157,19 @@ impl Keypair {
|
||||
PublicKey(self.0.public_key().serialize())
|
||||
}
|
||||
|
||||
/// Obtain the [`SecretKey`] part of this key pair. This should be kept secret.
|
||||
pub fn secret_key(&self) -> SecretKeyBytes {
|
||||
*self.0.secret_key().as_ref()
|
||||
}
|
||||
|
||||
/// Sign some message. These bytes can be used directly in a Substrate `MultiSignature::Ecdsa(..)`.
|
||||
pub fn sign(&self, message: &[u8]) -> Signature {
|
||||
let message_hash = sp_crypto_hashing::blake2_256(message);
|
||||
let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed");
|
||||
self.sign_prehashed(&sp_crypto_hashing::blake2_256(message))
|
||||
}
|
||||
|
||||
/// Signs a pre-hashed message.
|
||||
pub fn sign_prehashed(&self, message_hash: &[u8; 32]) -> Signature {
|
||||
let wrapped = Message::from_digest_slice(message_hash).expect("Message is 32 bytes; qed");
|
||||
Signature(internal::sign(&self.0.secret_key(), &wrapped))
|
||||
}
|
||||
}
|
||||
@@ -213,23 +221,30 @@ pub(crate) mod internal {
|
||||
}
|
||||
|
||||
/// An error handed back if creating a keypair fails.
|
||||
#[derive(Debug, PartialEq, Display, From)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
/// Invalid seed.
|
||||
#[display(fmt = "Invalid seed (was it the wrong length?)")]
|
||||
#[from(ignore)]
|
||||
InvalidSeed,
|
||||
/// Invalid seed.
|
||||
#[display(fmt = "Invalid seed for ECDSA, contained soft junction")]
|
||||
#[from(ignore)]
|
||||
SoftJunction,
|
||||
/// Invalid phrase.
|
||||
#[display(fmt = "Cannot parse phrase: {_0}")]
|
||||
Phrase(bip39::Error),
|
||||
/// Invalid hex.
|
||||
#[display(fmt = "Cannot parse hex string: {_0}")]
|
||||
Hex(hex::FromHexError),
|
||||
}
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Error::InvalidSeed => write!(f, "Invalid seed (was it the wrong length?)"),
|
||||
Error::SoftJunction => write!(f, "Invalid seed for ECDSA, contained soft junction"),
|
||||
Error::Phrase(e) => write!(f, "Cannot parse phrase: {e}"),
|
||||
Error::Hex(e) => write!(f, "Cannot parse hex string: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_from!(bip39::Error => Error::Phrase);
|
||||
impl_from!(hex::FromHexError => Error::Hex);
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
+102
-73
@@ -6,10 +6,8 @@
|
||||
|
||||
use crate::ecdsa;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use core::fmt::{Display, Formatter};
|
||||
use core::str::FromStr;
|
||||
use derive_more::Display;
|
||||
use keccak_hash::keccak;
|
||||
use secp256k1::Message;
|
||||
|
||||
@@ -18,6 +16,15 @@ const SECRET_KEY_LENGTH: usize = 32;
|
||||
/// Bytes representing a private key.
|
||||
pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH];
|
||||
|
||||
/// The public key for an [`Keypair`] key pair. This is the uncompressed variant of [`ecdsa::PublicKey`].
|
||||
pub struct PublicKey(pub [u8; 65]);
|
||||
|
||||
impl AsRef<[u8]> for PublicKey {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// An ethereum keypair implementation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Keypair(ecdsa::Keypair);
|
||||
@@ -90,29 +97,27 @@ impl Keypair {
|
||||
.map_err(|_| Error::InvalidSeed)
|
||||
}
|
||||
|
||||
/// Obtain the [`ecdsa::PublicKey`] of this keypair.
|
||||
pub fn public_key(&self) -> ecdsa::PublicKey {
|
||||
self.0.public_key()
|
||||
/// Obtain the [`ecdsa::SecretKeyBytes`] of this keypair.
|
||||
pub fn secret_key(&self) -> SecretKeyBytes {
|
||||
self.0.secret_key()
|
||||
}
|
||||
|
||||
/// Obtains the public address of the account by taking the last 20 bytes
|
||||
/// of the Keccak-256 hash of the public key.
|
||||
pub fn account_id(&self) -> AccountId20 {
|
||||
/// Obtain the [`eth::PublicKey`] of this keypair.
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
let uncompressed = self.0 .0.public_key().serialize_uncompressed();
|
||||
let hash = keccak(&uncompressed[1..]).0;
|
||||
let hash20 = hash[12..].try_into().expect("should be 20 bytes");
|
||||
AccountId20(hash20)
|
||||
PublicKey(uncompressed)
|
||||
}
|
||||
|
||||
/// Signs an arbitrary message payload.
|
||||
pub fn sign(&self, signer_payload: &[u8]) -> Signature {
|
||||
let message_hash = keccak(signer_payload);
|
||||
let wrapped =
|
||||
Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed");
|
||||
Signature(ecdsa::internal::sign(&self.0 .0.secret_key(), &wrapped))
|
||||
self.sign_prehashed(&keccak(signer_payload).0)
|
||||
}
|
||||
|
||||
/// Signs a pre-hashed message.
|
||||
pub fn sign_prehashed(&self, message_hash: &[u8; 32]) -> Signature {
|
||||
Signature(self.0.sign_prehashed(message_hash).0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A derivation path. This can be parsed from a valid derivation path string like
|
||||
/// `"m/44'/60'/0'/0/0"`, or we can construct one using the helpers [`DerivationPath::empty()`]
|
||||
/// and [`DerivationPath::eth()`].
|
||||
@@ -167,64 +172,55 @@ impl AsRef<[u8; 65]> for Signature {
|
||||
}
|
||||
}
|
||||
|
||||
/// A 20-byte cryptographic identifier.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, codec::Encode)]
|
||||
pub struct AccountId20(pub [u8; 20]);
|
||||
|
||||
impl AccountId20 {
|
||||
fn checksum(&self) -> String {
|
||||
let hex_address = hex::encode(self.0);
|
||||
let hash = keccak(hex_address.as_bytes());
|
||||
|
||||
let mut checksum_address = String::with_capacity(42);
|
||||
checksum_address.push_str("0x");
|
||||
|
||||
for (i, ch) in hex_address.chars().enumerate() {
|
||||
// Get the corresponding nibble from the hash
|
||||
let nibble = hash[i / 2] >> (if i % 2 == 0 { 4 } else { 0 }) & 0xf;
|
||||
|
||||
if nibble >= 8 {
|
||||
checksum_address.push(ch.to_ascii_uppercase());
|
||||
} else {
|
||||
checksum_address.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
checksum_address
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for AccountId20 {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AccountId20 {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{}", self.checksum())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &ecdsa::PublicKey) -> bool {
|
||||
/// Verify that some signature for a message was created by the owner of the [`PublicKey`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use subxt_signer::{ bip39::Mnemonic, eth };
|
||||
///
|
||||
/// let keypair = eth::dev::alith();
|
||||
/// let message = b"Hello!";
|
||||
///
|
||||
/// let signature = keypair.sign(message);
|
||||
/// let public_key = keypair.public_key();
|
||||
/// assert!(eth::verify(&signature, message, &public_key));
|
||||
/// ```
|
||||
pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &PublicKey) -> bool {
|
||||
let message_hash = keccak(message.as_ref());
|
||||
let wrapped =
|
||||
Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed");
|
||||
let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig.as_ref()[..64]) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(pk) = secp256k1::PublicKey::from_slice(&pubkey.0) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
ecdsa::internal::verify(&sig.0, &wrapped, pubkey)
|
||||
secp256k1::Secp256k1::verification_only()
|
||||
.verify_ecdsa(&wrapped, &signature, &pk)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// An error handed back if creating a keypair fails.
|
||||
#[derive(Debug, PartialEq, Display)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
/// Invalid seed.
|
||||
#[display(fmt = "Invalid seed (was it the wrong length?)")]
|
||||
InvalidSeed,
|
||||
/// Invalid derivation path.
|
||||
#[display(fmt = "Could not derive from path; some valeus in the path may have been >= 2^31?")]
|
||||
DeriveFromPath,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Error::InvalidSeed => write!(f, "Invalid seed (was it the wrong length?)"),
|
||||
Error::DeriveFromPath => write!(
|
||||
f,
|
||||
"Could not derive from path; some values in the path may have been >= 2^31?"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
@@ -267,36 +263,68 @@ pub mod dev {
|
||||
|
||||
#[cfg(feature = "subxt")]
|
||||
mod subxt_compat {
|
||||
use super::*;
|
||||
use subxt_core::config::Config;
|
||||
use subxt_core::tx::signer::Signer as SignerT;
|
||||
|
||||
use super::*;
|
||||
use subxt_core::utils::AccountId20;
|
||||
use subxt_core::utils::MultiAddress;
|
||||
|
||||
impl<T: Config> SignerT<T> for Keypair
|
||||
where
|
||||
T::AccountId: From<AccountId20>,
|
||||
T::Address: From<AccountId20>,
|
||||
T::AccountId: From<PublicKey>,
|
||||
T::Address: From<PublicKey>,
|
||||
T::Signature: From<Signature>,
|
||||
{
|
||||
fn account_id(&self) -> T::AccountId {
|
||||
self.account_id().into()
|
||||
self.public_key().into()
|
||||
}
|
||||
|
||||
fn address(&self) -> T::Address {
|
||||
self.account_id().into()
|
||||
self.public_key().into()
|
||||
}
|
||||
|
||||
fn sign(&self, signer_payload: &[u8]) -> T::Signature {
|
||||
self.sign(signer_payload).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
/// Obtains the public address of the account by taking the last 20 bytes
|
||||
/// of the Keccak-256 hash of the public key.
|
||||
pub fn to_account_id(&self) -> AccountId20 {
|
||||
let hash = keccak(&self.0[1..]).0;
|
||||
let hash20 = hash[12..].try_into().expect("should be 20 bytes");
|
||||
AccountId20(hash20)
|
||||
}
|
||||
/// A shortcut to obtain a [`MultiAddress`] from a [`PublicKey`].
|
||||
/// We often want this type, and using this method avoids any
|
||||
/// ambiguous type resolution issues.
|
||||
pub fn to_address<T>(self) -> MultiAddress<AccountId20, T> {
|
||||
MultiAddress::Address20(self.to_account_id().0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for AccountId20 {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
value.to_account_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<PublicKey> for MultiAddress<AccountId20, T> {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
let address: AccountId20 = value.into();
|
||||
MultiAddress::Address20(address.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "subxt")]
|
||||
mod test {
|
||||
use bip39::Mnemonic;
|
||||
use proptest::prelude::*;
|
||||
use secp256k1::Secp256k1;
|
||||
use subxt_core::utils::AccountId20;
|
||||
|
||||
use subxt_core::{config::*, tx::signer::Signer as SignerT, utils::H256};
|
||||
|
||||
@@ -369,7 +397,7 @@ mod test {
|
||||
fn check_subxt_signer_implementation_matches(keypair in keypair(), msg in ".*") {
|
||||
let msg_as_bytes = msg.as_bytes();
|
||||
|
||||
assert_eq!(SubxtSigner::account_id(&keypair), keypair.account_id());
|
||||
assert_eq!(SubxtSigner::account_id(&keypair), keypair.public_key().to_account_id());
|
||||
assert_eq!(SubxtSigner::sign(&keypair, msg_as_bytes), keypair.sign(msg_as_bytes));
|
||||
}
|
||||
|
||||
@@ -382,8 +410,9 @@ mod test {
|
||||
let hash20 = hash[12..].try_into().expect("should be 20 bytes");
|
||||
AccountId20(hash20)
|
||||
};
|
||||
|
||||
assert_eq!(keypair.account_id(), account_id);
|
||||
let account_id_derived_from_pk: AccountId20 = keypair.public_key().to_account_id();
|
||||
assert_eq!(account_id_derived_from_pk, account_id);
|
||||
assert_eq!(keypair.public_key().to_account_id(), account_id);
|
||||
|
||||
}
|
||||
|
||||
@@ -442,7 +471,7 @@ mod test {
|
||||
];
|
||||
|
||||
for (case_idx, (keypair, exp_account_id, exp_priv_key)) in cases.into_iter().enumerate() {
|
||||
let act_account_id = keypair.account_id().to_string();
|
||||
let act_account_id = keypair.public_key().to_account_id().checksum();
|
||||
let act_priv_key = format!("0x{}", &keypair.0 .0.display_secret());
|
||||
|
||||
assert_eq!(
|
||||
@@ -587,7 +616,7 @@ mod test {
|
||||
fn test_account_derivation_1() {
|
||||
let kp = Keypair::from_secret_key(KEY_1).expect("valid keypair");
|
||||
assert_eq!(
|
||||
kp.account_id().to_string(),
|
||||
kp.public_key().to_account_id().checksum(),
|
||||
"0x976f8456E4e2034179B284A23C0e0c8f6d3da50c"
|
||||
);
|
||||
}
|
||||
@@ -596,7 +625,7 @@ mod test {
|
||||
fn test_account_derivation_2() {
|
||||
let kp = Keypair::from_secret_key(KEY_2).expect("valid keypair");
|
||||
assert_eq!(
|
||||
kp.account_id().to_string(),
|
||||
kp.public_key().to_account_id().checksum(),
|
||||
"0x420e9F260B40aF7E49440ceAd3069f8e82A5230f"
|
||||
);
|
||||
}
|
||||
@@ -605,7 +634,7 @@ mod test {
|
||||
fn test_account_derivation_3() {
|
||||
let kp = Keypair::from_secret_key(KEY_3).expect("valid keypair");
|
||||
assert_eq!(
|
||||
kp.account_id().to_string(),
|
||||
kp.public_key().to_account_id().checksum(),
|
||||
"0x9cce34F7aB185c7ABA1b7C8140d620B4BDA941d6"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ pub mod ecdsa;
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-eth")))]
|
||||
pub mod eth;
|
||||
|
||||
/// A polkadot-js account json loader.
|
||||
#[cfg(feature = "polkadot-js-compat")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "polkadot-js-compat")))]
|
||||
pub mod polkadot_js_compat;
|
||||
|
||||
// Re-export useful bits and pieces for generating a Pair from a phrase,
|
||||
// namely the Mnemonic struct.
|
||||
pub use bip39;
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! A Polkadot-JS account loader.
|
||||
|
||||
use base64::Engine;
|
||||
use core::fmt::Display;
|
||||
use crypto_secretbox::{
|
||||
aead::{Aead, KeyInit},
|
||||
Key, Nonce, XSalsa20Poly1305,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use subxt_core::utils::AccountId32;
|
||||
|
||||
use crate::sr25519;
|
||||
|
||||
/// Given a JSON keypair as exported from Polkadot-JS, this returns an [`sr25519::Keypair`]
|
||||
pub fn decrypt_json(json: &str, password: &str) -> Result<sr25519::Keypair, Error> {
|
||||
let pair_json: KeyringPairJson = serde_json::from_str(json)?;
|
||||
Ok(pair_json.decrypt(password)?)
|
||||
}
|
||||
|
||||
/// Error
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Error decoding JSON.
|
||||
Json(serde_json::Error),
|
||||
/// The keypair has an unsupported encoding.
|
||||
UnsupportedEncoding,
|
||||
/// Base64 decoding error.
|
||||
Base64(base64::DecodeError),
|
||||
/// Wrong Scrypt parameters
|
||||
UnsupportedScryptParameters {
|
||||
/// N
|
||||
n: u32,
|
||||
/// p
|
||||
p: u32,
|
||||
/// r
|
||||
r: u32,
|
||||
},
|
||||
/// Decryption error.
|
||||
Secretbox(crypto_secretbox::Error),
|
||||
/// sr25519 keypair error.
|
||||
Sr25519(sr25519::Error),
|
||||
/// The decrypted keys are not valid.
|
||||
InvalidKeys,
|
||||
}
|
||||
|
||||
impl_from!(serde_json::Error => Error::Json);
|
||||
impl_from!(base64::DecodeError => Error::Base64);
|
||||
impl_from!(crypto_secretbox::Error => Error::Secretbox);
|
||||
impl_from!(sr25519::Error => Error::Sr25519);
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Error::Json(e) => write!(f, "Invalid JSON: {e}"),
|
||||
Error::UnsupportedEncoding => write!(f, "Unsupported encoding."),
|
||||
Error::Base64(e) => write!(f, "Base64 decoding error: {e}"),
|
||||
Error::UnsupportedScryptParameters { n, p, r } => {
|
||||
write!(f, "Unsupported Scrypt parameters: N: {n}, p: {p}, r: {r}")
|
||||
}
|
||||
Error::Secretbox(e) => write!(f, "Decryption error: {e}"),
|
||||
Error::Sr25519(e) => write!(f, "{e}"),
|
||||
Error::InvalidKeys => write!(f, "The decrypted keys are not valid."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EncryptionMetadata {
|
||||
/// Descriptor for the content
|
||||
content: Vec<String>,
|
||||
/// The encoding (in current/latest versions this is always an array)
|
||||
r#type: Vec<String>,
|
||||
/// The version of encoding applied
|
||||
version: String,
|
||||
}
|
||||
|
||||
/// https://github.com/polkadot-js/common/blob/37fa211fdb141d4f6eb32e8f377a4651ed2d9068/packages/keyring/src/types.ts#L67
|
||||
#[derive(Deserialize)]
|
||||
struct KeyringPairJson {
|
||||
/// The encoded string
|
||||
encoded: String,
|
||||
/// The encoding used
|
||||
encoding: EncryptionMetadata,
|
||||
/// The ss58 encoded address or the hex-encoded version (the latter is for ETH-compat chains)
|
||||
address: AccountId32,
|
||||
}
|
||||
|
||||
// This can be removed once split_array is stabilized.
|
||||
fn slice_to_u32(slice: &[u8]) -> u32 {
|
||||
u32::from_le_bytes(slice.try_into().expect("Slice should be 4 bytes."))
|
||||
}
|
||||
|
||||
impl KeyringPairJson {
|
||||
/// Decrypt JSON keypair.
|
||||
fn decrypt(self, password: &str) -> Result<sr25519::Keypair, Error> {
|
||||
// Check encoding.
|
||||
// https://github.com/polkadot-js/common/blob/37fa211fdb141d4f6eb32e8f377a4651ed2d9068/packages/keyring/src/keyring.ts#L166
|
||||
if self.encoding.version != "3"
|
||||
|| !self.encoding.content.contains(&"pkcs8".to_owned())
|
||||
|| !self.encoding.content.contains(&"sr25519".to_owned())
|
||||
|| !self.encoding.r#type.contains(&"scrypt".to_owned())
|
||||
|| !self
|
||||
.encoding
|
||||
.r#type
|
||||
.contains(&"xsalsa20-poly1305".to_owned())
|
||||
{
|
||||
return Err(Error::UnsupportedEncoding);
|
||||
}
|
||||
|
||||
// Decode from Base64.
|
||||
let decoded = base64::engine::general_purpose::STANDARD.decode(self.encoded)?;
|
||||
let params: [u8; 68] = decoded[..68]
|
||||
.try_into()
|
||||
.map_err(|_| Error::UnsupportedEncoding)?;
|
||||
|
||||
// Extract scrypt parameters.
|
||||
// https://github.com/polkadot-js/common/blob/master/packages/util-crypto/src/scrypt/fromU8a.ts
|
||||
let salt = ¶ms[0..32];
|
||||
let n = slice_to_u32(¶ms[32..36]);
|
||||
let p = slice_to_u32(¶ms[36..40]);
|
||||
let r = slice_to_u32(¶ms[40..44]);
|
||||
|
||||
// FIXME At this moment we assume these to be fixed params, this is not a great idea
|
||||
// since we lose flexibility and updates for greater security. However we need some
|
||||
// protection against carefully-crafted params that can eat up CPU since these are user
|
||||
// inputs. So we need to get very clever here, but atm we only allow the defaults
|
||||
// and if no match, bail out.
|
||||
if n != 32768 || p != 1 || r != 8 {
|
||||
return Err(Error::UnsupportedScryptParameters { n, p, r });
|
||||
}
|
||||
|
||||
// Hash password.
|
||||
let scrypt_params =
|
||||
scrypt::Params::new(15, 8, 1, 32).expect("Provided parameters should be valid.");
|
||||
let mut key = Key::default();
|
||||
scrypt::scrypt(password.as_bytes(), salt, &scrypt_params, &mut key)
|
||||
.expect("Key should be 32 bytes.");
|
||||
|
||||
// Decrypt keys.
|
||||
// https://github.com/polkadot-js/common/blob/master/packages/util-crypto/src/json/decryptData.ts
|
||||
let cipher = XSalsa20Poly1305::new(&key);
|
||||
let nonce = Nonce::from_slice(¶ms[44..68]);
|
||||
let ciphertext = &decoded[68..];
|
||||
let plaintext = cipher.decrypt(nonce, ciphertext)?;
|
||||
|
||||
// https://github.com/polkadot-js/common/blob/master/packages/keyring/src/pair/decode.ts
|
||||
if plaintext.len() != 117 {
|
||||
return Err(Error::InvalidKeys);
|
||||
}
|
||||
|
||||
let header = &plaintext[0..16];
|
||||
let secret_key = &plaintext[16..80];
|
||||
let div = &plaintext[80..85];
|
||||
let public_key = &plaintext[85..117];
|
||||
|
||||
if header != [48, 83, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]
|
||||
|| div != [161, 35, 3, 33, 0]
|
||||
{
|
||||
return Err(Error::InvalidKeys);
|
||||
}
|
||||
|
||||
// Generate keypair.
|
||||
let keypair = sr25519::Keypair::from_ed25519_bytes(secret_key)?;
|
||||
|
||||
// Ensure keys are correct.
|
||||
if keypair.public_key().0 != public_key
|
||||
|| keypair.public_key().to_account_id() != self.address
|
||||
{
|
||||
return Err(Error::InvalidKeys);
|
||||
}
|
||||
|
||||
Ok(keypair)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_keypair_sr25519() {
|
||||
let json = r#"
|
||||
{
|
||||
"encoded": "DumgApKCTqoCty1OZW/8WS+sgo6RdpHhCwAkA2IoDBMAgAAAAQAAAAgAAAB6IG/q24EeVf0JqWqcBd5m2tKq5BlyY84IQ8oamLn9DZe9Ouhgunr7i36J1XxUnTI801axqL/ym1gil0U8440Qvj0lFVKwGuxq38zuifgoj0B3Yru0CI6QKEvQPU5xxj4MpyxdSxP+2PnTzYao0HDH0fulaGvlAYXfqtU89xrx2/z9z7IjSwS3oDFPXRQ9kAdDebtyCVreZ9Otw9v3",
|
||||
"encoding": {
|
||||
"content": [
|
||||
"pkcs8",
|
||||
"sr25519"
|
||||
],
|
||||
"type": [
|
||||
"scrypt",
|
||||
"xsalsa20-poly1305"
|
||||
],
|
||||
"version": "3"
|
||||
},
|
||||
"address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
|
||||
"meta": {
|
||||
"genesisHash": "",
|
||||
"name": "Alice",
|
||||
"whenCreated": 1718265838755
|
||||
}
|
||||
}
|
||||
"#;
|
||||
decrypt_json(json, "whoisalice").unwrap();
|
||||
}
|
||||
}
|
||||
+31
-7
@@ -4,11 +4,10 @@
|
||||
|
||||
//! An sr25519 keypair implementation.
|
||||
|
||||
use core::str::FromStr;
|
||||
use core::{fmt::Display, str::FromStr};
|
||||
|
||||
use crate::crypto::{seed_from_entropy, DeriveJunction, SecretUri};
|
||||
|
||||
use derive_more::{Display, From};
|
||||
use hex::FromHex;
|
||||
use schnorrkel::{
|
||||
derive::{ChainCode, Derivation},
|
||||
@@ -123,6 +122,18 @@ impl Keypair {
|
||||
Ok(Keypair(keypair))
|
||||
}
|
||||
|
||||
/// Construct a keypair from a slice of bytes, corresponding to
|
||||
/// an Ed25519 expanded secret key.
|
||||
#[cfg(feature = "polkadot-js-compat")]
|
||||
pub(crate) fn from_ed25519_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
let secret_key = schnorrkel::SecretKey::from_ed25519_bytes(bytes)?;
|
||||
|
||||
Ok(Keypair(schnorrkel::Keypair {
|
||||
public: secret_key.to_public(),
|
||||
secret: secret_key,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Derive a child key from this one given a series of junctions.
|
||||
///
|
||||
/// # Example
|
||||
@@ -192,18 +203,31 @@ pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &PublicKey) -
|
||||
}
|
||||
|
||||
/// An error handed back if creating a keypair fails.
|
||||
#[derive(Debug, Display, From)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Invalid seed.
|
||||
#[display(fmt = "Invalid seed (was it the wrong length?)")]
|
||||
#[from(ignore)]
|
||||
InvalidSeed,
|
||||
/// Invalid phrase.
|
||||
#[display(fmt = "Cannot parse phrase: {_0}")]
|
||||
Phrase(bip39::Error),
|
||||
/// Invalid hex.
|
||||
#[display(fmt = "Cannot parse hex string: {_0}")]
|
||||
Hex(hex::FromHexError),
|
||||
/// Signature error.
|
||||
Signature(schnorrkel::SignatureError),
|
||||
}
|
||||
|
||||
impl_from!(bip39::Error => Error::Phrase);
|
||||
impl_from!(hex::FromHexError => Error::Hex);
|
||||
impl_from!(schnorrkel::SignatureError => Error::Signature);
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Error::InvalidSeed => write!(f, "Invalid seed (was it the wrong length?)"),
|
||||
Error::Phrase(e) => write!(f, "Cannot parse phrase: {e}"),
|
||||
Error::Hex(e) => write!(f, "Cannot parse hex string: {e}"),
|
||||
Error::Signature(e) => write!(f, "Signature error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
|
||||
@@ -21,6 +21,7 @@ macro_rules! once_static_cloned {
|
||||
($($(#[$attr:meta])* $vis:vis fn $name:ident() -> $ty:ty { $expr:expr } )+) => {
|
||||
$(
|
||||
$(#[$attr])*
|
||||
#[allow(missing_docs)]
|
||||
$vis fn $name() -> $ty {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "std")] {
|
||||
@@ -34,3 +35,13 @@ macro_rules! once_static_cloned {
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($module_path:path => $delegate_ty:ident :: $variant:ident) => {
|
||||
impl From<$module_path> for $delegate_ty {
|
||||
fn from(val: $module_path) -> Self {
|
||||
$delegate_ty::$variant(val.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+27
-10
@@ -26,9 +26,11 @@ default = ["jsonrpsee", "native"]
|
||||
# Exactly 1 of "web" and "native" is expected.
|
||||
native = [
|
||||
"jsonrpsee?/async-client",
|
||||
"jsonrpsee?/client-ws-transport-native-tls",
|
||||
"jsonrpsee?/client-ws-transport-tls",
|
||||
"jsonrpsee?/ws-client",
|
||||
"subxt-lightclient?/native",
|
||||
"tokio-util"
|
||||
"tokio-util",
|
||||
"tokio?/sync",
|
||||
]
|
||||
|
||||
# Enable this for web/wasm builds.
|
||||
@@ -36,14 +38,17 @@ native = [
|
||||
web = [
|
||||
"jsonrpsee?/async-wasm-client",
|
||||
"jsonrpsee?/client-web-transport",
|
||||
"jsonrpsee?/wasm-client",
|
||||
"getrandom/js",
|
||||
"subxt-lightclient?/web",
|
||||
"subxt-macro/web",
|
||||
"instant/wasm-bindgen"
|
||||
"instant/wasm-bindgen",
|
||||
"tokio?/sync",
|
||||
"finito?/wasm-bindgen",
|
||||
]
|
||||
|
||||
# Enable this to use the reconnecting rpc client
|
||||
unstable-reconnecting-rpc-client = ["dep:reconnecting-jsonrpsee-ws-client"]
|
||||
unstable-reconnecting-rpc-client = ["dep:finito", "dep:tokio", "jsonrpsee", "wasm-bindgen-futures"]
|
||||
|
||||
# Enable this to use jsonrpsee (allowing for example `OnlineClient::from_url`).
|
||||
jsonrpsee = [
|
||||
@@ -65,6 +70,9 @@ unstable-metadata = []
|
||||
# Note that this feature is experimental and things may break or not work as expected.
|
||||
unstable-light-client = ["subxt-lightclient"]
|
||||
|
||||
# Activate this to expose the ability to generate metadata from Wasm runtime files.
|
||||
runtime-path = ["subxt-macro/runtime-path"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] }
|
||||
@@ -98,9 +106,6 @@ subxt-core = { workspace = true, features = ["std"] }
|
||||
subxt-metadata = { workspace = true, features = ["std"] }
|
||||
subxt-lightclient = { workspace = true, optional = true, default-features = false }
|
||||
|
||||
# Reconnecting jsonrpc ws client
|
||||
reconnecting-jsonrpsee-ws-client = { version = "0.4", optional = true }
|
||||
|
||||
# For parsing urls to disallow insecure schemes
|
||||
url = { workspace = true }
|
||||
|
||||
@@ -110,11 +115,18 @@ getrandom = { workspace = true, optional = true }
|
||||
# Included if "native" feature is enabled
|
||||
tokio-util = { workspace = true, features = ["compat"], optional = true }
|
||||
|
||||
# Included if the reconnecting rpc client feature is enabled
|
||||
# Only the `tokio/sync` is used in the reconnecting rpc client
|
||||
# and that compiles both for native and web.
|
||||
tokio = { workspace = true, optional = true }
|
||||
finito = { workspace = true, optional = true }
|
||||
wasm-bindgen-futures = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
bitvec = { workspace = true }
|
||||
codec = { workspace = true, features = ["derive", "bit-vec"] }
|
||||
scale-info = { workspace = true, features = ["bit-vec"] }
|
||||
tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread"] }
|
||||
tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread", "sync"] }
|
||||
sp-core = { workspace = true }
|
||||
sp-keyring = { workspace = true }
|
||||
sp-runtime = { workspace = true }
|
||||
@@ -125,6 +137,11 @@ subxt-signer = { path = "../signer", features = ["unstable-eth"] }
|
||||
# the light-client wlll emit INFO logs with
|
||||
# `GrandPa warp sync finished` and `Finalized block runtime ready.`
|
||||
tracing-subscriber = { workspace = true }
|
||||
# These deps are needed to test the reconnecting rpc client
|
||||
jsonrpsee = { workspace = true, features = ["server"] }
|
||||
tower = "0.4"
|
||||
hyper = "1"
|
||||
http-body = "1"
|
||||
|
||||
[[example]]
|
||||
name = "light_client_basic"
|
||||
@@ -137,8 +154,8 @@ path = "examples/light_client_local_node.rs"
|
||||
required-features = ["unstable-light-client", "jsonrpsee", "native"]
|
||||
|
||||
[[example]]
|
||||
name = "reconnecting_rpc_client"
|
||||
path = "examples/reconnecting_rpc_client.rs"
|
||||
name = "setup_reconnecting_rpc_client"
|
||||
path = "examples/setup_reconnecting_rpc_client.rs"
|
||||
required-features = ["unstable-reconnecting-rpc-client"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
//! Example to utilize the `reconnecting rpc client` in subxt
|
||||
//! which hidden behind behind `--feature unstable-reconnecting-rpc-client`
|
||||
//!
|
||||
//! To utilize full logs from the RPC client use:
|
||||
//! `RUST_LOG="jsonrpsee=trace,reconnecting_jsonrpsee_ws_client=trace"`
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use subxt::backend::rpc::reconnecting_rpc_client::{Client, ExponentialBackoff, PingConfig};
|
||||
use subxt::backend::rpc::RpcClient;
|
||||
use subxt::error::{Error, RpcError};
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
// Generate an interface that we can use from the node's metadata.
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Create a new client with with a reconnecting RPC client.
|
||||
let rpc = Client::builder()
|
||||
// Reconnect with exponential backoff
|
||||
//
|
||||
// This API is "iterator-like" so one could limit it to only
|
||||
// reconnect x times and then quit.
|
||||
.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
|
||||
// Send period WebSocket pings/pongs every 6th second and if it's not ACK:ed in 30 seconds
|
||||
// then disconnect.
|
||||
//
|
||||
// This is just a way to ensure that the connection isn't idle if no message is sent that often
|
||||
.enable_ws_ping(
|
||||
PingConfig::new()
|
||||
.ping_interval(Duration::from_secs(6))
|
||||
.inactive_limit(Duration::from_secs(30)),
|
||||
)
|
||||
// There are other configurations as well that can be found here:
|
||||
// <https://docs.rs/reconnecting-jsonrpsee-ws-client/latest/reconnecting_jsonrpsee_ws_client/struct.ClientBuilder.html>
|
||||
.build("ws://localhost:9944".to_string())
|
||||
.await?;
|
||||
|
||||
let api: OnlineClient<PolkadotConfig> =
|
||||
OnlineClient::from_rpc_client(RpcClient::new(rpc.clone())).await?;
|
||||
|
||||
// Subscribe to all finalized blocks:
|
||||
let mut blocks_sub = api.blocks().subscribe_finalized().await?;
|
||||
|
||||
// For each block, print a bunch of information about it:
|
||||
while let Some(block) = blocks_sub.next().await {
|
||||
let block = match block {
|
||||
Ok(b) => b,
|
||||
Err(Error::Rpc(RpcError::DisconnectedWillReconnect(err))) => {
|
||||
println!("{err}");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
let block_number = block.header().number;
|
||||
let block_hash = block.hash();
|
||||
|
||||
println!("Block #{block_number} ({block_hash})");
|
||||
}
|
||||
|
||||
println!("RPC client reconnected `{}` times", rpc.reconnect_count());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -37,6 +37,7 @@ impl Config for CustomConfig {
|
||||
signed_extensions::CheckMortality<Self>,
|
||||
signed_extensions::ChargeAssetTxPayment<Self>,
|
||||
signed_extensions::ChargeTransactionPayment,
|
||||
signed_extensions::CheckMetadataHash,
|
||||
// And add a new one of our own:
|
||||
CustomSignedExtension,
|
||||
),
|
||||
@@ -83,8 +84,8 @@ impl ExtrinsicParamsEncoder for CustomSignedExtension {
|
||||
pub fn custom(
|
||||
params: DefaultExtrinsicParamsBuilder<CustomConfig>,
|
||||
) -> <<CustomConfig as Config>::ExtrinsicParams as ExtrinsicParams<CustomConfig>>::Params {
|
||||
let (a, b, c, d, e, f, g) = params.build();
|
||||
(a, b, c, d, e, f, g, ())
|
||||
let (a, b, c, d, e, f, g, h) = params.build();
|
||||
(a, b, c, d, e, f, g, h, ())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Example to utilize the `reconnecting rpc client` in subxt
|
||||
//! which hidden behind behind `--feature unstable-reconnecting-rpc-client`
|
||||
//!
|
||||
//! To utilize full logs from the RPC client use:
|
||||
//! `RUST_LOG="jsonrpsee=trace,subxt-reconnecting-rpc-client=trace"`
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::StreamExt;
|
||||
use subxt::backend::rpc::reconnecting_rpc_client::{ExponentialBackoff, RpcClient};
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
// Generate an interface that we can use from the node's metadata.
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Create a new client with with a reconnecting RPC client.
|
||||
let rpc = RpcClient::builder()
|
||||
// Reconnect with exponential backoff
|
||||
//
|
||||
// This API is "iterator-like" and we use `take` to limit the number of retries.
|
||||
.retry_policy(
|
||||
ExponentialBackoff::from_millis(100)
|
||||
.max_delay(Duration::from_secs(10))
|
||||
.take(3),
|
||||
)
|
||||
// There are other configurations as well that can be found at [`reconnecting_rpc_client::ClientBuilder`].
|
||||
.build("ws://localhost:9944".to_string())
|
||||
.await?;
|
||||
|
||||
// If you want to use the unstable backend with the reconnecting RPC client, you can do so like this:
|
||||
//
|
||||
// ```
|
||||
// use subxt::backend::unstable::UnstableBackend;
|
||||
// use subxt::OnlineClient;
|
||||
//
|
||||
// let (backend, mut driver) = UnstableBackend::builder().build(RpcClient::new(rpc.clone()));
|
||||
// tokio::spawn(async move {
|
||||
// while let Some(val) = driver.next().await {
|
||||
// if let Err(e) = val {
|
||||
// eprintln!("Error driving unstable backend: {e}; terminating client");
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// let api: OnlineClient<PolkadotConfig> = OnlineClient::from_backend(Arc::new(backend)).await?;
|
||||
// ```
|
||||
|
||||
let api: OnlineClient<PolkadotConfig> = OnlineClient::from_rpc_client(rpc.clone()).await?;
|
||||
|
||||
// Run for at most 100 blocks and print a bunch of information about it.
|
||||
//
|
||||
// The subscription is automatically re-started when the RPC client has reconnected.
|
||||
// You can test that by stopping the polkadot node and restarting it.
|
||||
let mut blocks_sub = api.blocks().subscribe_finalized().await?.take(100);
|
||||
|
||||
while let Some(block) = blocks_sub.next().await {
|
||||
let block = match block {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
// This can only happen on the legacy backend and the unstable backend
|
||||
// will handle this internally.
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
println!("The RPC connection was lost and we may have missed a few blocks");
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
let block_number = block.number();
|
||||
let block_hash = block.hash();
|
||||
|
||||
println!("Block #{block_number} ({block_hash})");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -22,10 +22,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.storage()
|
||||
.at_latest()
|
||||
.await?
|
||||
.fetch_raw(subxt_core::storage::get_address_bytes(&storage_query, &api.metadata()).unwrap())
|
||||
.fetch(&storage_query)
|
||||
.await?;
|
||||
|
||||
let v = hex::encode(result.unwrap());
|
||||
let v = result.unwrap().data.free;
|
||||
println!("Alice: {v}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,7 +6,8 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use subxt::OnlineClient;
|
||||
use subxt_signer::eth::{dev, AccountId20, Signature};
|
||||
use subxt_core::utils::AccountId20;
|
||||
use subxt_signer::eth::{dev, Signature};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/frontier_metadata_small.scale")]
|
||||
mod eth_runtime {}
|
||||
@@ -25,28 +26,20 @@ impl subxt::Config for EthRuntimeConfig {
|
||||
type AssetId = u32;
|
||||
}
|
||||
|
||||
// This helper makes it easy to use our `AccountId20`'s with generated
|
||||
// code that expects a generated `eth_runtime::runtime_types::fp_account:AccountId20` type.
|
||||
impl From<AccountId20> for eth_runtime::runtime_types::fp_account::AccountId20 {
|
||||
fn from(a: AccountId20) -> Self {
|
||||
eth_runtime::runtime_types::fp_account::AccountId20(a.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api = OnlineClient::<EthRuntimeConfig>::from_insecure_url("ws://127.0.0.1:9944").await?;
|
||||
|
||||
let alith = dev::alith();
|
||||
let baltathar = dev::baltathar();
|
||||
let dest = baltathar.account_id();
|
||||
let dest = baltathar.public_key().to_account_id();
|
||||
|
||||
println!("baltathar pub: {}", hex::encode(baltathar.public_key().0));
|
||||
println!("baltathar addr: {}", hex::encode(dest));
|
||||
|
||||
let balance_transfer_tx = eth_runtime::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(dest.into(), 10_001);
|
||||
.transfer_allow_death(dest, 10_001);
|
||||
|
||||
let events = api
|
||||
.tx()
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use subxt_signer::sr25519::dev;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Prepare some extrinsics. These are boxed so that they can live alongside each other.
|
||||
let txs = [dynamic_remark(), balance_transfer(), remark()];
|
||||
|
||||
for tx in txs {
|
||||
let from = dev::alice();
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&tx, &from)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
println!("Submitted tx");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn balance_transfer() -> Box<dyn subxt::tx::Payload> {
|
||||
let dest = dev::bob().public_key().into();
|
||||
Box::new(polkadot::tx().balances().transfer_allow_death(dest, 10_000))
|
||||
}
|
||||
|
||||
fn remark() -> Box<dyn subxt::tx::Payload> {
|
||||
Box::new(polkadot::tx().system().remark(vec![1, 2, 3, 4, 5]))
|
||||
}
|
||||
|
||||
fn dynamic_remark() -> Box<dyn subxt::tx::Payload> {
|
||||
use subxt::dynamic::{tx, Value};
|
||||
let tx_payload = tx("System", "remark", vec![Value::from_bytes("Hello")]);
|
||||
|
||||
Box::new(tx_payload)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use subxt_signer::sr25519::dev;
|
||||
|
||||
type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), BoxedError> {
|
||||
// Spawned tasks require things held across await points to impl Send,
|
||||
// so we use one to demonstrate that this is possible with `PartialExtrinsic`
|
||||
tokio::spawn(signing_example()).await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn signing_example() -> Result<(), BoxedError> {
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Build a balance transfer extrinsic.
|
||||
let dest = dev::bob().public_key().into();
|
||||
let balance_transfer_tx = polkadot::tx().balances().transfer_allow_death(dest, 10_000);
|
||||
|
||||
let alice = dev::alice();
|
||||
|
||||
// Create partial tx, ready to be signed.
|
||||
let partial_tx = api
|
||||
.tx()
|
||||
.create_partial_signed(
|
||||
&balance_transfer_tx,
|
||||
&alice.public_key().to_account_id(),
|
||||
Default::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Simulate taking some time to get a signature back, in part to
|
||||
// show that the `PartialExtrinsic` can be held across await points.
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
let signature = alice.sign(&partial_tx.signer_payload());
|
||||
|
||||
// Sign the transaction.
|
||||
let tx = partial_tx
|
||||
.sign_with_address_and_signature(&alice.public_key().to_address(), &signature.into());
|
||||
|
||||
// Submit it.
|
||||
tx.submit_and_watch()
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+158
-68
@@ -8,10 +8,12 @@
|
||||
pub mod rpc_methods;
|
||||
|
||||
use self::rpc_methods::TransactionStatus as RpcTransactionStatus;
|
||||
use crate::backend::utils::{retry, retry_stream};
|
||||
use crate::backend::{
|
||||
rpc::RpcClient, Backend, BlockRef, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
|
||||
TransactionStatus,
|
||||
};
|
||||
use crate::error::RpcError;
|
||||
use crate::{config::Header, Config, Error};
|
||||
use async_trait::async_trait;
|
||||
use futures::{future, future::Either, stream, Future, FutureExt, Stream, StreamExt};
|
||||
@@ -62,12 +64,21 @@ impl<T: Config> LegacyBackendBuilder<T> {
|
||||
}
|
||||
|
||||
/// The legacy backend.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct LegacyBackend<T> {
|
||||
storage_page_size: u32,
|
||||
methods: LegacyRpcMethods<T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for LegacyBackend<T> {
|
||||
fn clone(&self) -> LegacyBackend<T> {
|
||||
LegacyBackend {
|
||||
storage_page_size: self.storage_page_size,
|
||||
methods: self.methods.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> LegacyBackend<T> {
|
||||
/// Configure and construct an [`LegacyBackend`].
|
||||
pub fn builder() -> LegacyBackendBuilder<T> {
|
||||
@@ -84,16 +95,28 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
keys: Vec<Vec<u8>>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<StorageResponse>, Error> {
|
||||
fn get_entry<T: Config>(
|
||||
key: Vec<u8>,
|
||||
at: T::Hash,
|
||||
methods: LegacyRpcMethods<T>,
|
||||
) -> impl Future<Output = Result<Option<StorageResponse>, Error>> {
|
||||
retry(move || {
|
||||
let methods = methods.clone();
|
||||
let key = key.clone();
|
||||
async move {
|
||||
let res = methods.state_get_storage(&key, Some(at)).await?;
|
||||
Ok(res.map(move |value| StorageResponse { key, value }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let keys = keys.clone();
|
||||
let methods = self.methods.clone();
|
||||
|
||||
// For each key, return it + a future to get the result.
|
||||
let iter = keys.into_iter().map(move |key| {
|
||||
let methods = methods.clone();
|
||||
async move {
|
||||
let res = methods.state_get_storage(&key, Some(at)).await?;
|
||||
Ok(res.map(|value| StorageResponse { key, value }))
|
||||
}
|
||||
});
|
||||
let iter = keys
|
||||
.into_iter()
|
||||
.map(move |key| get_entry(key, at, methods.clone()));
|
||||
|
||||
let s = stream::iter(iter)
|
||||
// Resolve the future
|
||||
@@ -158,99 +181,159 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
}
|
||||
|
||||
async fn genesis_hash(&self) -> Result<T::Hash, Error> {
|
||||
self.methods.genesis_hash().await
|
||||
retry(|| self.methods.genesis_hash()).await
|
||||
}
|
||||
|
||||
async fn block_header(&self, at: T::Hash) -> Result<Option<T::Header>, Error> {
|
||||
self.methods.chain_get_header(Some(at)).await
|
||||
retry(|| self.methods.chain_get_header(Some(at))).await
|
||||
}
|
||||
|
||||
async fn block_body(&self, at: T::Hash) -> Result<Option<Vec<Vec<u8>>>, Error> {
|
||||
let Some(details) = self.methods.chain_get_block(Some(at)).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(
|
||||
details.block.extrinsics.into_iter().map(|b| b.0).collect(),
|
||||
))
|
||||
retry(|| async {
|
||||
let Some(details) = self.methods.chain_get_block(Some(at)).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(
|
||||
details.block.extrinsics.into_iter().map(|b| b.0).collect(),
|
||||
))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn latest_finalized_block_ref(&self) -> Result<BlockRef<T::Hash>, Error> {
|
||||
let hash = self.methods.chain_get_finalized_head().await?;
|
||||
Ok(BlockRef::from_hash(hash))
|
||||
retry(|| async {
|
||||
let hash = self.methods.chain_get_finalized_head().await?;
|
||||
Ok(BlockRef::from_hash(hash))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn current_runtime_version(&self) -> Result<RuntimeVersion, Error> {
|
||||
let details = self.methods.state_get_runtime_version(None).await?;
|
||||
Ok(RuntimeVersion {
|
||||
spec_version: details.spec_version,
|
||||
transaction_version: details.transaction_version,
|
||||
retry(|| async {
|
||||
let details = self.methods.state_get_runtime_version(None).await?;
|
||||
Ok(RuntimeVersion {
|
||||
spec_version: details.spec_version,
|
||||
transaction_version: details.transaction_version,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stream_runtime_version(&self) -> Result<StreamOfResults<RuntimeVersion>, Error> {
|
||||
let sub = self.methods.state_subscribe_runtime_version().await?;
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|v| RuntimeVersion {
|
||||
spec_version: v.spec_version,
|
||||
transaction_version: v.transaction_version,
|
||||
let methods = self.methods.clone();
|
||||
|
||||
let retry_sub = retry_stream(move || {
|
||||
let methods = methods.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let sub = methods.state_subscribe_runtime_version().await?;
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|v| RuntimeVersion {
|
||||
spec_version: v.spec_version,
|
||||
transaction_version: v.transaction_version,
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
// For runtime version subscriptions we omit the `DisconnectedWillReconnect` error
|
||||
// because the once it resubscribes it will emit the latest runtime version.
|
||||
//
|
||||
// Thus, it's technically possible that a runtime version can be missed if
|
||||
// two runtime upgrades happen in quick succession, but this is very unlikely.
|
||||
let stream = retry_sub.filter(|r| {
|
||||
let forward = !matches!(r, Err(Error::Rpc(RpcError::DisconnectedWillReconnect(_))));
|
||||
async move { forward }
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
|
||||
Ok(StreamOf(Box::pin(stream)))
|
||||
}
|
||||
|
||||
async fn stream_all_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
|
||||
let sub = self.methods.chain_subscribe_all_heads().await?;
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash();
|
||||
(h, BlockRef::from_hash(hash))
|
||||
let methods = self.methods.clone();
|
||||
|
||||
let retry_sub = retry_stream(move || {
|
||||
let methods = methods.clone();
|
||||
Box::pin(async move {
|
||||
let sub = methods.chain_subscribe_all_heads().await?;
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash();
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(retry_sub)
|
||||
}
|
||||
|
||||
async fn stream_best_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
|
||||
let sub = self.methods.chain_subscribe_new_heads().await?;
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash();
|
||||
(h, BlockRef::from_hash(hash))
|
||||
let methods = self.methods.clone();
|
||||
|
||||
let retry_sub = retry_stream(move || {
|
||||
let methods = methods.clone();
|
||||
Box::pin(async move {
|
||||
let sub = methods.chain_subscribe_new_heads().await?;
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash();
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(retry_sub)
|
||||
}
|
||||
|
||||
async fn stream_finalized_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
|
||||
let sub: super::rpc::RpcSubscription<<T as Config>::Header> =
|
||||
self.methods.chain_subscribe_finalized_heads().await?;
|
||||
let this = self.clone();
|
||||
|
||||
// Get the last finalized block immediately so that the stream will emit every finalized block after this.
|
||||
let last_finalized_block_ref = self.latest_finalized_block_ref().await?;
|
||||
let last_finalized_block_num = self
|
||||
.block_header(last_finalized_block_ref.hash())
|
||||
.await?
|
||||
.map(|h| h.number().into());
|
||||
let retry_sub = retry_stream(move || {
|
||||
let this = this.clone();
|
||||
Box::pin(async move {
|
||||
let sub = this.methods.chain_subscribe_finalized_heads().await?;
|
||||
|
||||
// Fill in any missing blocks, because the backend may not emit every finalized block; just the latest ones which
|
||||
// are finalized each time.
|
||||
let sub = subscribe_to_block_headers_filling_in_gaps(
|
||||
self.methods.clone(),
|
||||
sub,
|
||||
last_finalized_block_num,
|
||||
);
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash();
|
||||
(h, BlockRef::from_hash(hash))
|
||||
// Get the last finalized block immediately so that the stream will emit every finalized block after this.
|
||||
let last_finalized_block_ref = this.latest_finalized_block_ref().await?;
|
||||
let last_finalized_block_num = this
|
||||
.block_header(last_finalized_block_ref.hash())
|
||||
.await?
|
||||
.map(|h| h.number().into());
|
||||
|
||||
// Fill in any missing blocks, because the backend may not emit every finalized block; just the latest ones which
|
||||
// are finalized each time.
|
||||
let sub = subscribe_to_block_headers_filling_in_gaps(
|
||||
this.methods.clone(),
|
||||
sub,
|
||||
last_finalized_block_num,
|
||||
);
|
||||
let sub = sub.map(|r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash();
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(retry_sub)
|
||||
}
|
||||
|
||||
async fn submit_transaction(
|
||||
@@ -261,6 +344,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
.methods
|
||||
.author_submit_and_watch_extrinsic(extrinsic)
|
||||
.await?;
|
||||
|
||||
let sub = sub.filter_map(|r| {
|
||||
let mapped = r
|
||||
.map(|tx| {
|
||||
@@ -309,7 +393,8 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
|
||||
future::ready(mapped)
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
|
||||
Ok(StreamOf::new(Box::pin(sub)))
|
||||
}
|
||||
|
||||
async fn call(
|
||||
@@ -318,9 +403,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: T::Hash,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
self.methods
|
||||
.state_call(method, call_parameters, Some(at))
|
||||
.await
|
||||
retry(|| self.methods.state_call(method, call_parameters, Some(at))).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,6 +514,11 @@ impl<T: Config> Stream for StorageFetchDescendantKeysStream<T> {
|
||||
return Poll::Ready(Some(Ok(keys)));
|
||||
}
|
||||
Err(e) => {
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
this.keys_fut = Some(keys_fut);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Error getting keys? Return it.
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
}
|
||||
@@ -513,7 +601,9 @@ impl<T: Config> Stream for StorageFetchDescendantValuesStream<T> {
|
||||
let at = this.keys.at;
|
||||
let results_fut = async move {
|
||||
let keys = keys.iter().map(|k| &**k);
|
||||
let values = methods.state_query_storage_at(keys, Some(at)).await?;
|
||||
let values =
|
||||
retry(|| methods.state_query_storage_at(keys.clone(), Some(at)))
|
||||
.await?;
|
||||
let values: VecDeque<_> = values
|
||||
.into_iter()
|
||||
.flat_map(|v| {
|
||||
|
||||
@@ -332,8 +332,7 @@ impl<T: Config> LegacyRpcMethods<T> {
|
||||
public: Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let params = rpc_params![key_type, suri, Bytes(public)];
|
||||
self.client.request("author_insertKey", params).await?;
|
||||
Ok(())
|
||||
self.client.request("author_insertKey", params).await
|
||||
}
|
||||
|
||||
/// Generate new session keys and returns the corresponding public keys.
|
||||
@@ -455,6 +454,7 @@ pub type EncodedJustification = Vec<u8>;
|
||||
/// the RPC call `state_getRuntimeVersion`,
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(test, derive(serde::Serialize))]
|
||||
pub struct RuntimeVersion {
|
||||
/// Version of the runtime specification. A full-node will not attempt to use its native
|
||||
/// runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
pub mod legacy;
|
||||
pub mod rpc;
|
||||
pub mod unstable;
|
||||
pub mod utils;
|
||||
|
||||
use subxt_core::client::RuntimeVersion;
|
||||
|
||||
@@ -324,9 +325,413 @@ pub enum TransactionStatus<Hash> {
|
||||
|
||||
/// A response from calls like [`Backend::storage_fetch_values`] or
|
||||
/// [`Backend::storage_fetch_descendant_values`].
|
||||
#[cfg_attr(test, derive(serde::Serialize, Clone, PartialEq, Debug))]
|
||||
pub struct StorageResponse {
|
||||
/// The key.
|
||||
pub key: Vec<u8>,
|
||||
/// The associated value.
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
mod legacy {
|
||||
use super::rpc::{RpcClient, RpcClientT};
|
||||
use crate::backend::rpc::RawRpcSubscription;
|
||||
use crate::backend::BackendExt;
|
||||
use crate::{
|
||||
backend::{
|
||||
legacy::rpc_methods::Bytes, legacy::rpc_methods::RuntimeVersion,
|
||||
legacy::LegacyBackend, StorageResponse,
|
||||
},
|
||||
error::RpcError,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use serde::Serialize;
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::Arc,
|
||||
};
|
||||
use subxt_core::{config::DefaultExtrinsicParams, Config};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
type RpcResult<T> = Result<T, RpcError>;
|
||||
type Item = RpcResult<String>;
|
||||
|
||||
struct MockDataTable {
|
||||
items: HashMap<Vec<u8>, VecDeque<Item>>,
|
||||
}
|
||||
|
||||
impl MockDataTable {
|
||||
fn new() -> Self {
|
||||
MockDataTable {
|
||||
items: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_iter<'a, T: Serialize, I: IntoIterator<Item = (&'a str, RpcResult<T>)>>(
|
||||
item: I,
|
||||
) -> Self {
|
||||
let mut data = Self::new();
|
||||
for (key, item) in item.into_iter() {
|
||||
data.push(key.into(), item);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn push<I: Serialize>(&mut self, key: Vec<u8>, item: RpcResult<I>) {
|
||||
let item = item.map(|x| serde_json::to_string(&x).unwrap());
|
||||
match self.items.entry(key) {
|
||||
std::collections::hash_map::Entry::Occupied(v) => v.into_mut().push_back(item),
|
||||
std::collections::hash_map::Entry::Vacant(e) => {
|
||||
e.insert(VecDeque::from([item]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pop(&mut self, key: Vec<u8>) -> Item {
|
||||
self.items.get_mut(&key).unwrap().pop_front().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
struct Subscription {
|
||||
sender: mpsc::Sender<RpcResult<Vec<Item>>>,
|
||||
receiver: mpsc::Receiver<RpcResult<Vec<Item>>>,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
fn new() -> Self {
|
||||
let (sender, receiver) = mpsc::channel(32);
|
||||
Self { sender, receiver }
|
||||
}
|
||||
|
||||
async fn from_iter<
|
||||
T: Serialize,
|
||||
S: IntoIterator<Item = RpcResult<Vec<RpcResult<T>>>>,
|
||||
>(
|
||||
items: S,
|
||||
) -> Self {
|
||||
let sub = Self::new();
|
||||
for i in items {
|
||||
let i: RpcResult<Vec<Item>> = i.map(|items| {
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| item.map(|i| serde_json::to_string(&i).unwrap()))
|
||||
.collect()
|
||||
});
|
||||
sub.write(i).await
|
||||
}
|
||||
sub
|
||||
}
|
||||
|
||||
async fn read(&mut self) -> RpcResult<Vec<Item>> {
|
||||
self.receiver.recv().await.unwrap()
|
||||
}
|
||||
|
||||
async fn write(&self, items: RpcResult<Vec<Item>>) {
|
||||
self.sender.send(items).await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
struct Data {
|
||||
request: MockDataTable,
|
||||
subscription: Subscription,
|
||||
}
|
||||
|
||||
struct MockRpcClientStorage {
|
||||
data: Arc<Mutex<Data>>,
|
||||
}
|
||||
|
||||
impl RpcClientT for MockRpcClientStorage {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<serde_json::value::RawValue>>,
|
||||
) -> super::rpc::RawRpcFuture<'a, Box<serde_json::value::RawValue>> {
|
||||
Box::pin(async move {
|
||||
match method {
|
||||
"state_getStorage" => {
|
||||
let mut data = self.data.lock().await;
|
||||
let params = params.map(|p| p.get().to_string());
|
||||
let rpc_params = jsonrpsee::types::Params::new(params.as_deref());
|
||||
let key: sp_core::Bytes = rpc_params.sequence().next().unwrap();
|
||||
let value = data.request.pop(key.0);
|
||||
value.map(|v| serde_json::value::RawValue::from_string(v).unwrap())
|
||||
}
|
||||
"chain_getBlockHash" => {
|
||||
let mut data = self.data.lock().await;
|
||||
let value = data.request.pop("chain_getBlockHash".into());
|
||||
value.map(|v| serde_json::value::RawValue::from_string(v).unwrap())
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
_sub: &'a str,
|
||||
_params: Option<Box<serde_json::value::RawValue>>,
|
||||
_unsub: &'a str,
|
||||
) -> super::rpc::RawRpcFuture<'a, super::rpc::RawRpcSubscription> {
|
||||
Box::pin(async {
|
||||
let mut data = self.data.lock().await;
|
||||
let values: RpcResult<Vec<RpcResult<Box<RawValue>>>> =
|
||||
data.subscription.read().await.map(|v| {
|
||||
v.into_iter()
|
||||
.map(|v| {
|
||||
v.map(|v| serde_json::value::RawValue::from_string(v).unwrap())
|
||||
})
|
||||
.collect::<Vec<RpcResult<Box<RawValue>>>>()
|
||||
});
|
||||
values.map(|v| RawRpcSubscription {
|
||||
stream: futures::stream::iter(v).boxed(),
|
||||
id: Some("ID".to_string()),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Define dummy config
|
||||
enum Conf {}
|
||||
impl Config for Conf {
|
||||
type Hash = crate::utils::H256;
|
||||
type AccountId = crate::utils::AccountId32;
|
||||
type Address = crate::utils::MultiAddress<Self::AccountId, ()>;
|
||||
type Signature = crate::utils::MultiSignature;
|
||||
type Hasher = crate::config::substrate::BlakeTwo256;
|
||||
type Header = crate::config::substrate::SubstrateHeader<u32, Self::Hasher>;
|
||||
type ExtrinsicParams = DefaultExtrinsicParams<Self>;
|
||||
|
||||
type AssetId = u32;
|
||||
}
|
||||
|
||||
use crate::backend::Backend;
|
||||
|
||||
fn client_runtime_version(num: u32) -> crate::client::RuntimeVersion {
|
||||
crate::client::RuntimeVersion {
|
||||
spec_version: num,
|
||||
transaction_version: num,
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_version(num: u32) -> RuntimeVersion {
|
||||
RuntimeVersion {
|
||||
spec_version: num,
|
||||
transaction_version: num,
|
||||
other: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes(str: &str) -> RpcResult<Option<Bytes>> {
|
||||
Ok(Some(Bytes(str.into())))
|
||||
}
|
||||
|
||||
fn storage_response<K: Into<Vec<u8>>, V: Into<Vec<u8>>>(key: K, value: V) -> StorageResponse
|
||||
where
|
||||
Vec<u8>: From<K>,
|
||||
{
|
||||
StorageResponse {
|
||||
key: key.into(),
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_mock_client<
|
||||
'a,
|
||||
T: Serialize,
|
||||
D: IntoIterator<Item = (&'a str, RpcResult<T>)>,
|
||||
S: IntoIterator<Item = RpcResult<Vec<RpcResult<T>>>>,
|
||||
>(
|
||||
table_data: D,
|
||||
subscription_data: S,
|
||||
) -> RpcClient {
|
||||
let data = Data {
|
||||
request: MockDataTable::from_iter(table_data),
|
||||
subscription: Subscription::from_iter(subscription_data).await,
|
||||
};
|
||||
RpcClient::new(MockRpcClientStorage {
|
||||
data: Arc::new(Mutex::new(data)),
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_fetch_values() {
|
||||
let mock_data = vec![
|
||||
("ID1", bytes("Data1")),
|
||||
(
|
||||
"ID2",
|
||||
Err(RpcError::DisconnectedWillReconnect(
|
||||
"Reconnecting".to_string(),
|
||||
)),
|
||||
),
|
||||
("ID2", bytes("Data2")),
|
||||
(
|
||||
"ID3",
|
||||
Err(RpcError::DisconnectedWillReconnect(
|
||||
"Reconnecting".to_string(),
|
||||
)),
|
||||
),
|
||||
("ID3", bytes("Data3")),
|
||||
];
|
||||
let rpc_client = build_mock_client(mock_data, vec![]).await;
|
||||
let backend: LegacyBackend<Conf> = LegacyBackend::builder().build(rpc_client);
|
||||
|
||||
// Test
|
||||
let response = backend
|
||||
.storage_fetch_values(
|
||||
["ID1".into(), "ID2".into(), "ID3".into()].into(),
|
||||
crate::utils::H256::random(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let response = response
|
||||
.map(|x| x.unwrap())
|
||||
.collect::<Vec<StorageResponse>>()
|
||||
.await;
|
||||
|
||||
let expected = vec![
|
||||
storage_response("ID1", "Data1"),
|
||||
storage_response("ID2", "Data2"),
|
||||
storage_response("ID3", "Data3"),
|
||||
];
|
||||
|
||||
assert_eq!(expected, response)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_fetch_value() {
|
||||
// Setup
|
||||
let mock_data = [
|
||||
(
|
||||
"ID1",
|
||||
Err(RpcError::DisconnectedWillReconnect(
|
||||
"Reconnecting".to_string(),
|
||||
)),
|
||||
),
|
||||
("ID1", bytes("Data1")),
|
||||
];
|
||||
let rpc_client = build_mock_client(mock_data, vec![]).await;
|
||||
|
||||
// Test
|
||||
let backend: LegacyBackend<Conf> = LegacyBackend::builder().build(rpc_client);
|
||||
let response = backend
|
||||
.storage_fetch_value("ID1".into(), crate::utils::H256::random())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let response = response.unwrap();
|
||||
assert_eq!("Data1".to_owned(), String::from_utf8(response).unwrap())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
/// This test should cover the logic of the following methods:
|
||||
/// - `genesis_hash`
|
||||
/// - `block_header`
|
||||
/// - `block_body`
|
||||
/// - `latest_finalized_block`
|
||||
/// - `current_runtime_version`
|
||||
/// - `current_runtime_version`
|
||||
/// - `call`
|
||||
/// The test covers them because they follow the simple pattern of:
|
||||
/// ```no_run
|
||||
/// async fn THE_THING(&self) -> Result<T::Hash, Error> {
|
||||
/// retry(|| <DO THE THING> ).await
|
||||
/// }
|
||||
/// ```
|
||||
async fn simple_fetch() {
|
||||
let hash = crate::utils::H256::random();
|
||||
let mock_data = vec![
|
||||
(
|
||||
"chain_getBlockHash",
|
||||
Err(RpcError::DisconnectedWillReconnect(
|
||||
"Reconnecting".to_string(),
|
||||
)),
|
||||
),
|
||||
("chain_getBlockHash", Ok(Some(hash))),
|
||||
];
|
||||
let rpc_client = build_mock_client(mock_data, vec![]).await;
|
||||
|
||||
// Test
|
||||
let backend: LegacyBackend<Conf> = LegacyBackend::builder().build(rpc_client);
|
||||
let response = backend.genesis_hash().await.unwrap();
|
||||
|
||||
assert_eq!(hash, response)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
/// This test should cover the logic of the following methods:
|
||||
/// - `stream_runtime_version`
|
||||
/// - `stream_all_block_headers`
|
||||
/// - `stream_best_block_headers`
|
||||
/// The test covers them because they follow the simple pattern of:
|
||||
/// ```no_run
|
||||
/// async fn stream_the_thing(
|
||||
/// &self,
|
||||
/// ) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
|
||||
/// let methods = self.methods.clone();
|
||||
/// let retry_sub = retry_stream(move || {
|
||||
/// let methods = methods.clone();
|
||||
/// Box::pin(async move {
|
||||
/// methods.do_the_thing().await?
|
||||
/// });
|
||||
/// Ok(StreamOf(Box::pin(sub)))
|
||||
/// })
|
||||
/// })
|
||||
/// .await?;
|
||||
/// Ok(retry_sub)
|
||||
/// }
|
||||
/// ```
|
||||
async fn stream_simple() {
|
||||
let mock_subscription_data = vec![
|
||||
Ok(vec![
|
||||
Ok(runtime_version(0)),
|
||||
Err(RpcError::DisconnectedWillReconnect(
|
||||
"Reconnecting".to_string(),
|
||||
)),
|
||||
Ok(runtime_version(1)),
|
||||
]),
|
||||
Ok(vec![
|
||||
Err(RpcError::DisconnectedWillReconnect(
|
||||
"Reconnecting".to_string(),
|
||||
)),
|
||||
Ok(runtime_version(2)),
|
||||
Ok(runtime_version(3)),
|
||||
]),
|
||||
Ok(vec![
|
||||
Ok(runtime_version(4)),
|
||||
Ok(runtime_version(5)),
|
||||
Err(RpcError::RequestRejected("Reconnecting".to_string())),
|
||||
]),
|
||||
];
|
||||
let rpc_client = build_mock_client(vec![], mock_subscription_data).await;
|
||||
|
||||
// Test
|
||||
let backend: LegacyBackend<Conf> = LegacyBackend::builder().build(rpc_client);
|
||||
|
||||
let mut results = backend.stream_runtime_version().await.unwrap();
|
||||
let mut expected = VecDeque::from(vec![
|
||||
Ok::<crate::client::RuntimeVersion, crate::Error>(client_runtime_version(0)),
|
||||
Ok(client_runtime_version(4)),
|
||||
Ok(client_runtime_version(5)),
|
||||
]);
|
||||
|
||||
while let Some(res) = results.next().await {
|
||||
if res.is_ok() {
|
||||
assert_eq!(expected.pop_front().unwrap().unwrap(), res.unwrap())
|
||||
} else {
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(crate::Error::Rpc(RpcError::RequestRejected(_)))
|
||||
))
|
||||
}
|
||||
}
|
||||
assert!(expected.is_empty());
|
||||
assert!(results.next().await.is_none())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,8 @@ crate::macros::cfg_unstable_light_client! {
|
||||
}
|
||||
|
||||
crate::macros::cfg_reconnecting_rpc_client! {
|
||||
mod reconnecting_jsonrpsee_impl;
|
||||
pub use reconnecting_jsonrpsee_ws_client as reconnecting_rpc_client;
|
||||
/// reconnecting rpc client.
|
||||
pub mod reconnecting_rpc_client;
|
||||
}
|
||||
|
||||
mod rpc_client;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
|
||||
use crate::error::RpcError;
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use reconnecting_jsonrpsee_ws_client::SubscriptionId;
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
impl RpcClientT for reconnecting_jsonrpsee_ws_client::Client {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>> {
|
||||
async {
|
||||
self.request_raw(method.to_string(), params)
|
||||
.await
|
||||
.map_err(|e| RpcError::ClientError(Box::new(e)))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
async {
|
||||
let sub = self
|
||||
.subscribe_raw(sub.to_string(), params, unsub.to_string())
|
||||
.await
|
||||
.map_err(|e| RpcError::ClientError(Box::new(e)))?;
|
||||
|
||||
let id = match sub.id() {
|
||||
SubscriptionId::Num(n) => n.to_string(),
|
||||
SubscriptionId::Str(s) => s.to_string(),
|
||||
};
|
||||
let stream = sub
|
||||
.map_err(|e| RpcError::DisconnectedWillReconnect(e.to_string()))
|
||||
.boxed();
|
||||
|
||||
Ok(RawRpcSubscription {
|
||||
stream,
|
||||
id: Some(id),
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # reconnecting-jsonrpsee-ws-client
|
||||
//!
|
||||
//! A simple reconnecting JSON-RPC WebSocket client for subxt which
|
||||
//! automatically reconnects when the connection is lost but
|
||||
//! it doesn't retain subscriptions and pending method calls when it reconnects.
|
||||
//!
|
||||
//! The logic which action to take for individual calls and subscriptions are
|
||||
//! handled by the subxt backend implementations.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use std::time::Duration;
|
||||
//! use futures::StreamExt;
|
||||
//! use subxt::backend::rpc::reconnecting_rpc_client::{RpcClient, ExponentialBackoff};
|
||||
//! use subxt::{OnlineClient, PolkadotConfig};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! let rpc = RpcClient::builder()
|
||||
//! .retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
|
||||
//! .build("ws://localhost:9944".to_string())
|
||||
//! .await
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let subxt_client: OnlineClient<PolkadotConfig> = OnlineClient::from_rpc_client(rpc.clone()).await.unwrap();
|
||||
//! let mut blocks_sub = subxt_client.blocks().subscribe_finalized().await.unwrap();
|
||||
//!
|
||||
//! while let Some(block) = blocks_sub.next().await {
|
||||
//! let block = match block {
|
||||
//! Ok(b) => b,
|
||||
//! Err(e) => {
|
||||
//! if e.is_disconnected_will_reconnect() {
|
||||
//! println!("The RPC connection was lost and we may have missed a few blocks");
|
||||
//! continue;
|
||||
//! } else {
|
||||
//! panic!("Error: {}", e);
|
||||
//! }
|
||||
//! }
|
||||
//! };
|
||||
//! println!("Block #{} ({})", block.number(), block.hash());
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
mod platform;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod utils;
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{self, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
|
||||
use crate::error::RpcError as SubxtRpcError;
|
||||
|
||||
use finito::Retry;
|
||||
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
|
||||
use jsonrpsee::core::{
|
||||
client::{
|
||||
Client as WsClient, ClientT, Subscription as RpcSubscription, SubscriptionClientT,
|
||||
SubscriptionKind,
|
||||
},
|
||||
traits::ToRpcParams,
|
||||
};
|
||||
use platform::spawn;
|
||||
use serde_json::value::RawValue;
|
||||
use tokio::sync::{
|
||||
mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
oneshot, Notify,
|
||||
};
|
||||
use utils::display_close_reason;
|
||||
|
||||
// re-exports
|
||||
pub use finito::{ExponentialBackoff, FibonacciBackoff, FixedInterval};
|
||||
pub use jsonrpsee::core::client::IdKind;
|
||||
pub use jsonrpsee::{core::client::error::Error as RpcError, rpc_params, types::SubscriptionId};
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub use jsonrpsee::ws_client::{HeaderMap, PingConfig};
|
||||
|
||||
const LOG_TARGET: &str = "subxt-reconnecting-rpc-client";
|
||||
|
||||
/// Method result.
|
||||
pub type MethodResult = Result<Box<RawValue>, Error>;
|
||||
/// Subscription result.
|
||||
pub type SubscriptionResult = Result<Box<RawValue>, DisconnectedWillReconnect>;
|
||||
|
||||
/// The connection was closed, reconnect initiated and the subscription was dropped.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("The connection was closed because of `{0:?}` and reconnect initiated")]
|
||||
pub struct DisconnectedWillReconnect(String);
|
||||
|
||||
/// New-type pattern which implements [`ToRpcParams`] that is required by jsonrpsee.
|
||||
#[derive(Debug, Clone)]
|
||||
struct RpcParams(Option<Box<RawValue>>);
|
||||
|
||||
impl ToRpcParams for RpcParams {
|
||||
fn to_rpc_params(self) -> Result<Option<Box<RawValue>>, serde_json::Error> {
|
||||
Ok(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Op {
|
||||
Call {
|
||||
method: String,
|
||||
params: RpcParams,
|
||||
send_back: oneshot::Sender<MethodResult>,
|
||||
},
|
||||
Subscription {
|
||||
subscribe_method: String,
|
||||
params: RpcParams,
|
||||
unsubscribe_method: String,
|
||||
send_back: oneshot::Sender<Result<Subscription, Error>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Error that can occur when for a RPC call or subscription.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The client was dropped by the user.
|
||||
#[error("The client was dropped")]
|
||||
Dropped,
|
||||
/// The connection was closed and reconnect initiated.
|
||||
#[error(transparent)]
|
||||
DisconnectedWillReconnect(#[from] DisconnectedWillReconnect),
|
||||
/// Other rpc error.
|
||||
#[error("{0}")]
|
||||
RpcError(RpcError),
|
||||
}
|
||||
|
||||
/// Represent a single subscription.
|
||||
pub struct Subscription {
|
||||
id: SubscriptionId<'static>,
|
||||
stream: mpsc::UnboundedReceiver<SubscriptionResult>,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
/// Returns the next notification from the stream.
|
||||
/// This may return `None` if the subscription has been terminated,
|
||||
/// which may happen if the channel becomes full or is dropped.
|
||||
///
|
||||
/// **Note:** This has an identical signature to the [`StreamExt::next`]
|
||||
/// method (and delegates to that). Import [`StreamExt`] if you'd like
|
||||
/// access to other stream combinator methods.
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub async fn next(&mut self) -> Option<SubscriptionResult> {
|
||||
StreamExt::next(self).await
|
||||
}
|
||||
|
||||
/// Get the subscription ID.
|
||||
pub fn id(&self) -> SubscriptionId<'static> {
|
||||
self.id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Subscription {
|
||||
type Item = SubscriptionResult;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut task::Context<'_>,
|
||||
) -> task::Poll<Option<Self::Item>> {
|
||||
match self.stream.poll_recv(cx) {
|
||||
Poll::Ready(Some(msg)) => Poll::Ready(Some(msg)),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Subscription {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Subscription")
|
||||
.field("id", &self.id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON-RPC client that reconnects automatically and may loose
|
||||
/// subscription notifications when it reconnects.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RpcClient {
|
||||
tx: mpsc::UnboundedSender<Op>,
|
||||
}
|
||||
|
||||
/// Builder for [`Client`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RpcClientBuilder<P> {
|
||||
max_request_size: u32,
|
||||
max_response_size: u32,
|
||||
retry_policy: P,
|
||||
#[cfg(feature = "native")]
|
||||
ping_config: Option<PingConfig>,
|
||||
#[cfg(feature = "native")]
|
||||
// web doesn't support custom headers
|
||||
// https://stackoverflow.com/a/4361358/6394734
|
||||
headers: HeaderMap,
|
||||
max_redirections: u32,
|
||||
id_kind: IdKind,
|
||||
max_log_len: u32,
|
||||
max_concurrent_requests: u32,
|
||||
request_timeout: Duration,
|
||||
connection_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for RpcClientBuilder<ExponentialBackoff> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_request_size: 10 * 1024 * 1024,
|
||||
max_response_size: 10 * 1024 * 1024,
|
||||
retry_policy: ExponentialBackoff::from_millis(10).max_delay(Duration::from_secs(60)),
|
||||
#[cfg(feature = "native")]
|
||||
ping_config: Some(PingConfig::new()),
|
||||
#[cfg(feature = "native")]
|
||||
headers: HeaderMap::new(),
|
||||
max_redirections: 5,
|
||||
id_kind: IdKind::Number,
|
||||
max_log_len: 1024,
|
||||
max_concurrent_requests: 1024,
|
||||
request_timeout: Duration::from_secs(60),
|
||||
connection_timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcClientBuilder<ExponentialBackoff> {
|
||||
/// Create a new builder.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> RpcClientBuilder<P>
|
||||
where
|
||||
P: Iterator<Item = Duration> + Send + Sync + 'static + Clone,
|
||||
{
|
||||
/// Configure the min response size a for websocket message.
|
||||
///
|
||||
/// Default: 10MB
|
||||
pub fn max_request_size(mut self, max: u32) -> Self {
|
||||
self.max_request_size = max;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the max response size a for websocket message.
|
||||
///
|
||||
/// Default: 10MB
|
||||
pub fn max_response_size(mut self, max: u32) -> Self {
|
||||
self.max_response_size = max;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the max number of redirections to perform until a connection is regarded as failed.
|
||||
///
|
||||
/// Default: 5
|
||||
pub fn max_redirections(mut self, redirect: u32) -> Self {
|
||||
self.max_redirections = redirect;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure how many concurrent method calls are allowed.
|
||||
///
|
||||
/// Default: 1024
|
||||
pub fn max_concurrent_requests(mut self, max: u32) -> Self {
|
||||
self.max_concurrent_requests = max;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure how long until a method call is regarded as failed.
|
||||
///
|
||||
/// Default: 1 minute
|
||||
pub fn request_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.request_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set connection timeout for the WebSocket handshake
|
||||
///
|
||||
/// Default: 10 seconds
|
||||
pub fn connection_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.connection_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the data type of the request object ID
|
||||
///
|
||||
/// Default: number
|
||||
pub fn id_format(mut self, kind: IdKind) -> Self {
|
||||
self.id_kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set maximum length for logging calls and responses.
|
||||
/// Logs bigger than this limit will be truncated.
|
||||
///
|
||||
/// Default: 1024
|
||||
pub fn set_max_logging_length(mut self, max: u32) -> Self {
|
||||
self.max_log_len = max;
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
|
||||
/// Configure custom headers to use in the WebSocket handshake.
|
||||
pub fn set_headers(mut self, headers: HeaderMap) -> Self {
|
||||
self.headers = headers;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure which retry policy to use when a connection is lost.
|
||||
///
|
||||
/// Default: Exponential backoff 10ms
|
||||
pub fn retry_policy<T>(self, retry_policy: T) -> RpcClientBuilder<T> {
|
||||
RpcClientBuilder {
|
||||
max_request_size: self.max_request_size,
|
||||
max_response_size: self.max_response_size,
|
||||
retry_policy,
|
||||
#[cfg(feature = "native")]
|
||||
ping_config: self.ping_config,
|
||||
#[cfg(feature = "native")]
|
||||
headers: self.headers,
|
||||
max_redirections: self.max_redirections,
|
||||
max_log_len: self.max_log_len,
|
||||
id_kind: self.id_kind,
|
||||
max_concurrent_requests: self.max_concurrent_requests,
|
||||
request_timeout: self.request_timeout,
|
||||
connection_timeout: self.connection_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
|
||||
/// Configure the WebSocket ping/pong interval.
|
||||
///
|
||||
/// Default: 30 seconds.
|
||||
pub fn enable_ws_ping(mut self, ping_config: PingConfig) -> Self {
|
||||
self.ping_config = Some(ping_config);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
|
||||
/// Disable WebSocket ping/pongs.
|
||||
///
|
||||
/// Default: 30 seconds.
|
||||
pub fn disable_ws_ping(mut self) -> Self {
|
||||
self.ping_config = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build and connect to the target.
|
||||
pub async fn build(self, url: String) -> Result<RpcClient, RpcError> {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let client = Retry::new(self.retry_policy.clone(), || {
|
||||
platform::ws_client(url.as_ref(), &self)
|
||||
})
|
||||
.await?;
|
||||
|
||||
platform::spawn(background_task(client, rx, url, self));
|
||||
|
||||
Ok(RpcClient { tx })
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcClient {
|
||||
/// Create a builder.
|
||||
pub fn builder() -> RpcClientBuilder<ExponentialBackoff> {
|
||||
RpcClientBuilder::new()
|
||||
}
|
||||
|
||||
/// Perform a JSON-RPC method call.
|
||||
pub async fn request(
|
||||
&self,
|
||||
method: String,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> Result<Box<RawValue>, Error> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(Op::Call {
|
||||
method,
|
||||
params: RpcParams(params),
|
||||
send_back: tx,
|
||||
})
|
||||
.map_err(|_| Error::Dropped)?;
|
||||
|
||||
rx.await.map_err(|_| Error::Dropped)?
|
||||
}
|
||||
|
||||
/// Perform a JSON-RPC subscription.
|
||||
pub async fn subscribe(
|
||||
&self,
|
||||
subscribe_method: String,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsubscribe_method: String,
|
||||
) -> Result<Subscription, Error> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(Op::Subscription {
|
||||
subscribe_method,
|
||||
params: RpcParams(params),
|
||||
unsubscribe_method,
|
||||
send_back: tx,
|
||||
})
|
||||
.map_err(|_| Error::Dropped)?;
|
||||
rx.await.map_err(|_| Error::Dropped)?
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcClientT for RpcClient {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>> {
|
||||
async {
|
||||
self.request(method.to_string(), params)
|
||||
.await
|
||||
.map_err(|e| SubxtRpcError::DisconnectedWillReconnect(e.to_string()))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
async {
|
||||
let sub = self
|
||||
.subscribe(sub.to_string(), params, unsub.to_string())
|
||||
.await
|
||||
.map_err(|e| SubxtRpcError::ClientError(Box::new(e)))?;
|
||||
|
||||
let id = match sub.id() {
|
||||
SubscriptionId::Num(n) => n.to_string(),
|
||||
SubscriptionId::Str(s) => s.to_string(),
|
||||
};
|
||||
let stream = sub
|
||||
.map_err(|e| SubxtRpcError::DisconnectedWillReconnect(e.to_string()))
|
||||
.boxed();
|
||||
|
||||
Ok(RawRpcSubscription {
|
||||
stream,
|
||||
id: Some(id),
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
async fn background_task<P>(
|
||||
mut client: Arc<WsClient>,
|
||||
mut rx: UnboundedReceiver<Op>,
|
||||
url: String,
|
||||
client_builder: RpcClientBuilder<P>,
|
||||
) where
|
||||
P: Iterator<Item = Duration> + Send + 'static + Clone,
|
||||
{
|
||||
let disconnect = Arc::new(tokio::sync::Notify::new());
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// An incoming JSON-RPC call to dispatch.
|
||||
next_message = rx.recv() => {
|
||||
match next_message {
|
||||
None => break,
|
||||
Some(op) => {
|
||||
spawn(dispatch_call(client.clone(), op, disconnect.clone()));
|
||||
}
|
||||
};
|
||||
}
|
||||
// The connection was terminated and try to reconnect.
|
||||
_ = client.on_disconnect() => {
|
||||
let params = ReconnectParams {
|
||||
url: &url,
|
||||
client_builder: &client_builder,
|
||||
close_reason: client.disconnect_reason().await,
|
||||
};
|
||||
|
||||
client = match reconnect(params).await {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
tracing::debug!(target: LOG_TARGET, "Failed to reconnect: {e}; terminating the connection");
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnect.notify_waiters();
|
||||
}
|
||||
|
||||
async fn dispatch_call(client: Arc<WsClient>, op: Op, on_disconnect: Arc<tokio::sync::Notify>) {
|
||||
match op {
|
||||
Op::Call {
|
||||
method,
|
||||
params,
|
||||
send_back,
|
||||
} => {
|
||||
match client.request::<Box<RawValue>, _>(&method, params).await {
|
||||
Ok(rp) => {
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Ok(rp));
|
||||
}
|
||||
Err(RpcError::RestartNeeded(e)) => {
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Err(DisconnectedWillReconnect(e.to_string()).into()));
|
||||
}
|
||||
Err(e) => {
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Err(Error::RpcError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Op::Subscription {
|
||||
subscribe_method,
|
||||
params,
|
||||
unsubscribe_method,
|
||||
send_back,
|
||||
} => {
|
||||
match client
|
||||
.subscribe::<Box<RawValue>, _>(
|
||||
&subscribe_method,
|
||||
params.clone(),
|
||||
&unsubscribe_method,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(sub) => {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let sub_id = match sub.kind() {
|
||||
SubscriptionKind::Subscription(id) => id.clone().into_owned(),
|
||||
_ => unreachable!("No method subscriptions possible in this crate; qed"),
|
||||
};
|
||||
|
||||
platform::spawn(subscription_handler(
|
||||
tx.clone(),
|
||||
sub,
|
||||
on_disconnect.clone(),
|
||||
client.clone(),
|
||||
));
|
||||
|
||||
let stream = Subscription {
|
||||
id: sub_id,
|
||||
stream: rx,
|
||||
};
|
||||
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Ok(stream));
|
||||
}
|
||||
Err(RpcError::RestartNeeded(e)) => {
|
||||
// Fails only if the request is dropped by the client.
|
||||
let _ = send_back.send(Err(DisconnectedWillReconnect(e.to_string()).into()));
|
||||
}
|
||||
Err(e) => {
|
||||
// Fails only if the request is dropped.
|
||||
let _ = send_back.send(Err(Error::RpcError(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for each individual subscription.
|
||||
async fn subscription_handler(
|
||||
sub_tx: UnboundedSender<SubscriptionResult>,
|
||||
mut rpc_sub: RpcSubscription<Box<RawValue>>,
|
||||
client_closed: Arc<Notify>,
|
||||
client: Arc<WsClient>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
next_msg = rpc_sub.next() => {
|
||||
let Some(notif) = next_msg else {
|
||||
let close = client.disconnect_reason().await;
|
||||
_ = sub_tx.send(Err(DisconnectedWillReconnect(close.to_string())));
|
||||
break;
|
||||
};
|
||||
|
||||
let msg = notif.expect("RawValue is valid JSON; qed");
|
||||
|
||||
// Fails only if subscription was closed by the user.
|
||||
if sub_tx.send(Ok(msg)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// This channel indices whether the subscription was closed by user.
|
||||
_ = sub_tx.closed() => {
|
||||
break;
|
||||
}
|
||||
// This channel indicates whether the main task has been closed.
|
||||
// at this point no further messages are processed.
|
||||
_ = client_closed.notified() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReconnectParams<'a, P> {
|
||||
url: &'a str,
|
||||
client_builder: &'a RpcClientBuilder<P>,
|
||||
close_reason: RpcError,
|
||||
}
|
||||
|
||||
async fn reconnect<P>(params: ReconnectParams<'_, P>) -> Result<Arc<WsClient>, RpcError>
|
||||
where
|
||||
P: Iterator<Item = Duration> + Send + 'static + Clone,
|
||||
{
|
||||
let ReconnectParams {
|
||||
url,
|
||||
client_builder,
|
||||
close_reason,
|
||||
} = params;
|
||||
|
||||
let retry_policy = client_builder.retry_policy.clone();
|
||||
|
||||
tracing::debug!(target: LOG_TARGET, "Connection to {url} was closed: `{}`; starting to reconnect", display_close_reason(&close_reason));
|
||||
|
||||
let client = Retry::new(retry_policy.clone(), || {
|
||||
platform::ws_client(url, client_builder)
|
||||
})
|
||||
.await?;
|
||||
|
||||
tracing::debug!(target: LOG_TARGET, "Connection to {url} was successfully re-established");
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::backend::rpc::reconnecting_rpc_client::{RpcClientBuilder, RpcError};
|
||||
use jsonrpsee::core::client::Client;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub use tokio::spawn;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
pub use wasm_bindgen_futures::spawn_local as spawn;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub async fn ws_client<P>(
|
||||
url: &str,
|
||||
builder: &RpcClientBuilder<P>,
|
||||
) -> Result<Arc<Client>, RpcError> {
|
||||
use jsonrpsee::ws_client::WsClientBuilder;
|
||||
|
||||
let RpcClientBuilder {
|
||||
max_request_size,
|
||||
max_response_size,
|
||||
ping_config,
|
||||
headers,
|
||||
max_redirections,
|
||||
id_kind,
|
||||
max_concurrent_requests,
|
||||
max_log_len,
|
||||
request_timeout,
|
||||
connection_timeout,
|
||||
..
|
||||
} = builder;
|
||||
|
||||
let mut ws_client_builder = WsClientBuilder::new()
|
||||
.max_request_size(*max_request_size)
|
||||
.max_response_size(*max_response_size)
|
||||
.set_headers(headers.clone())
|
||||
.max_redirections(*max_redirections as usize)
|
||||
.max_buffer_capacity_per_subscription(tokio::sync::Semaphore::MAX_PERMITS)
|
||||
.max_concurrent_requests(*max_concurrent_requests as usize)
|
||||
.set_max_logging_length(*max_log_len)
|
||||
.set_tcp_no_delay(true)
|
||||
.request_timeout(*request_timeout)
|
||||
.connection_timeout(*connection_timeout)
|
||||
.id_format(*id_kind);
|
||||
|
||||
if let Some(ping) = ping_config {
|
||||
ws_client_builder = ws_client_builder.enable_ws_ping(*ping);
|
||||
}
|
||||
|
||||
let client = ws_client_builder.build(url).await?;
|
||||
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
pub async fn ws_client<P>(
|
||||
url: &str,
|
||||
builder: &RpcClientBuilder<P>,
|
||||
) -> Result<Arc<Client>, RpcError> {
|
||||
use jsonrpsee::wasm_client::WasmClientBuilder;
|
||||
|
||||
let RpcClientBuilder {
|
||||
id_kind,
|
||||
max_concurrent_requests,
|
||||
max_log_len,
|
||||
request_timeout,
|
||||
..
|
||||
} = builder;
|
||||
|
||||
let ws_client_builder = WasmClientBuilder::new()
|
||||
.max_buffer_capacity_per_subscription(tokio::sync::Semaphore::MAX_PERMITS)
|
||||
.max_concurrent_requests(*max_concurrent_requests as usize)
|
||||
.set_max_logging_length(*max_log_len)
|
||||
.request_timeout(*request_timeout)
|
||||
.id_format(*id_kind);
|
||||
|
||||
let client = ws_client_builder.build(url).await?;
|
||||
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::*;
|
||||
use futures::{future::Either, FutureExt};
|
||||
|
||||
use jsonrpsee::core::BoxError;
|
||||
use jsonrpsee::server::{
|
||||
http, stop_channel, ws, ConnectionGuard, ConnectionState, HttpRequest, HttpResponse, RpcModule,
|
||||
RpcServiceBuilder, ServerConfig, SubscriptionMessage,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_works() {
|
||||
let (_handle, addr) = run_server().await.unwrap();
|
||||
let client = RpcClient::builder().build(addr).await.unwrap();
|
||||
assert!(client.request("say_hello".to_string(), None).await.is_ok(),)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sub_works() {
|
||||
let (_handle, addr) = run_server().await.unwrap();
|
||||
|
||||
let client = RpcClient::builder()
|
||||
.retry_policy(ExponentialBackoff::from_millis(50))
|
||||
.build(addr)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut sub = client
|
||||
.subscribe(
|
||||
"subscribe_lo".to_string(),
|
||||
None,
|
||||
"unsubscribe_lo".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(sub.next().await.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sub_with_reconnect() {
|
||||
let (handle, addr) = run_server().await.unwrap();
|
||||
let client = RpcClient::builder().build(addr.clone()).await.unwrap();
|
||||
|
||||
let mut sub = client
|
||||
.subscribe(
|
||||
"subscribe_lo".to_string(),
|
||||
None,
|
||||
"unsubscribe_lo".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _ = handle.send(());
|
||||
|
||||
// Hack to wait for the server to restart.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
assert!(matches!(sub.next().await, Some(Ok(_))));
|
||||
assert!(matches!(
|
||||
sub.next().await,
|
||||
Some(Err(DisconnectedWillReconnect(_)))
|
||||
));
|
||||
|
||||
// Restart the server.
|
||||
let (_handle, _) = run_server_with_settings(Some(&addr), false).await.unwrap();
|
||||
|
||||
// Hack to wait for the server to restart.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// Subscription should work after reconnect.
|
||||
let mut sub = client
|
||||
.subscribe(
|
||||
"subscribe_lo".to_string(),
|
||||
None,
|
||||
"unsubscribe_lo".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(sub.next().await, Some(Ok(_))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_with_reconnect() {
|
||||
let (handle, addr) = run_server_with_settings(None, true).await.unwrap();
|
||||
|
||||
let client = Arc::new(RpcClient::builder().build(addr.clone()).await.unwrap());
|
||||
|
||||
let req_fut = client.request("say_hello".to_string(), None).boxed();
|
||||
let timeout_fut = tokio::time::sleep(Duration::from_secs(5));
|
||||
|
||||
// If the call isn't replied in 5 secs then it's regarded as it's still pending.
|
||||
let req_fut = match futures::future::select(Box::pin(timeout_fut), req_fut).await {
|
||||
Either::Left((_, f)) => f,
|
||||
Either::Right(_) => panic!("RPC call finished"),
|
||||
};
|
||||
|
||||
// Close the connection with a pending call.
|
||||
let _ = handle.send(());
|
||||
|
||||
// Restart the server
|
||||
let (_handle, _) = run_server_with_settings(Some(&addr), false).await.unwrap();
|
||||
|
||||
// Hack to wait for the server to restart.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// This call should fail because reconnect.
|
||||
assert!(req_fut.await.is_err());
|
||||
// Future call should work after reconnect.
|
||||
assert!(client.request("say_hello".to_string(), None).await.is_ok());
|
||||
}
|
||||
|
||||
async fn run_server() -> Result<(tokio::sync::broadcast::Sender<()>, String), BoxError> {
|
||||
run_server_with_settings(None, false).await
|
||||
}
|
||||
|
||||
async fn run_server_with_settings(
|
||||
url: Option<&str>,
|
||||
dont_respond_to_method_calls: bool,
|
||||
) -> Result<(tokio::sync::broadcast::Sender<()>, String), BoxError> {
|
||||
use jsonrpsee::server::HttpRequest;
|
||||
|
||||
let sockaddr = match url {
|
||||
Some(url) => url.strip_prefix("ws://").unwrap(),
|
||||
None => "127.0.0.1:0",
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
let listener = loop {
|
||||
if let Ok(l) = tokio::net::TcpListener::bind(sockaddr).await {
|
||||
break l;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
if i >= 10 {
|
||||
panic!("Addr already in use");
|
||||
}
|
||||
|
||||
i += 1;
|
||||
};
|
||||
|
||||
let mut module = RpcModule::new(());
|
||||
|
||||
if dont_respond_to_method_calls {
|
||||
module.register_async_method("say_hello", |_, _, _| async {
|
||||
futures::future::pending::<()>().await;
|
||||
"timeout"
|
||||
})?;
|
||||
} else {
|
||||
module.register_async_method("say_hello", |_, _, _| async { "lo" })?;
|
||||
}
|
||||
|
||||
module.register_subscription(
|
||||
"subscribe_lo",
|
||||
"subscribe_lo",
|
||||
"unsubscribe_lo",
|
||||
|_params, pending, _ctx, _| async move {
|
||||
let sink = pending.accept().await.unwrap();
|
||||
let i = 0;
|
||||
|
||||
loop {
|
||||
if sink
|
||||
.send(SubscriptionMessage::from_json(&i).unwrap())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::broadcast::channel(4);
|
||||
let tx2 = tx.clone();
|
||||
let (stop_handle, server_handle) = stop_channel();
|
||||
let addr = listener.local_addr().expect("Could not find local addr");
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let sock = tokio::select! {
|
||||
res = listener.accept() => {
|
||||
match res {
|
||||
Ok((stream, _remote_addr)) => stream,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to accept connection: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = rx.recv() => {
|
||||
break
|
||||
}
|
||||
};
|
||||
|
||||
let module = module.clone();
|
||||
let rx2 = tx2.subscribe();
|
||||
let tx2 = tx2.clone();
|
||||
let stop_handle2 = stop_handle.clone();
|
||||
|
||||
let svc = tower::service_fn(move |req: HttpRequest<hyper::body::Incoming>| {
|
||||
let module = module.clone();
|
||||
let tx = tx2.clone();
|
||||
let stop_handle = stop_handle2.clone();
|
||||
|
||||
let conn_permit = ConnectionGuard::new(1).try_acquire().unwrap();
|
||||
|
||||
if ws::is_upgrade_request(&req) {
|
||||
let rpc_service = RpcServiceBuilder::new();
|
||||
let conn = ConnectionState::new(stop_handle, 1, conn_permit);
|
||||
|
||||
async move {
|
||||
let mut rx = tx.subscribe();
|
||||
|
||||
let (rp, conn_fut) =
|
||||
ws::connect(req, ServerConfig::default(), module, conn, rpc_service)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = conn_fut => (),
|
||||
_ = rx.recv() => {},
|
||||
}
|
||||
});
|
||||
|
||||
Ok::<_, BoxError>(rp)
|
||||
}
|
||||
.boxed()
|
||||
} else {
|
||||
async { Ok(http::response::denied()) }.boxed()
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(serve_with_graceful_shutdown(sock, svc, rx2));
|
||||
}
|
||||
|
||||
drop(server_handle);
|
||||
});
|
||||
|
||||
Ok((tx, format!("ws://{}", addr)))
|
||||
}
|
||||
|
||||
async fn serve_with_graceful_shutdown<S, B, I>(
|
||||
io: I,
|
||||
service: S,
|
||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) where
|
||||
S: tower::Service<HttpRequest<hyper::body::Incoming>, Response = HttpResponse<B>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ 'static,
|
||||
S::Future: Send,
|
||||
S::Response: Send,
|
||||
S::Error: Into<BoxError>,
|
||||
B: http_body::Body<Data = hyper::body::Bytes> + Send + 'static,
|
||||
B::Error: Into<BoxError>,
|
||||
I: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
|
||||
{
|
||||
if let Err(e) =
|
||||
jsonrpsee::server::serve_with_graceful_shutdown(io, service, rx.recv().map(|_| ())).await
|
||||
{
|
||||
tracing::error!("Error while serving: {:?}", e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Utils.
|
||||
|
||||
use crate::backend::rpc::reconnecting_rpc_client::RpcError;
|
||||
|
||||
pub fn display_close_reason(err: &RpcError) -> String {
|
||||
match err {
|
||||
RpcError::RestartNeeded(e) => e.to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,12 @@ impl<Hash> Stream for FollowStream<Hash> {
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Err(e)) => {
|
||||
// Re-start if a reconnecting backend was enabled.
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
this.stream = InnerStreamState::Stopped;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Finish forever if there's an error, passing it on.
|
||||
this.stream = InnerStreamState::Finished;
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
@@ -182,6 +188,12 @@ impl<Hash> Stream for FollowStream<Hash> {
|
||||
return Poll::Ready(Some(Ok(FollowStreamMsg::Event(ev))));
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => {
|
||||
// Re-start if a reconnecting backend was enabled.
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
this.stream = InnerStreamState::Stopped;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Finish forever if there's an error, passing it on.
|
||||
this.stream = InnerStreamState::Finished;
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use super::follow_stream_unpin::{BlockRef, FollowStreamMsg, FollowStreamUnpin};
|
||||
use crate::backend::unstable::rpc_methods::{FollowEvent, Initialized, RuntimeEvent};
|
||||
use crate::config::BlockHash;
|
||||
use crate::error::Error;
|
||||
use crate::error::{Error, RpcError};
|
||||
use futures::stream::{Stream, StreamExt};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::ops::DerefMut;
|
||||
@@ -267,8 +267,9 @@ impl<Hash: BlockHash> Shared<Hash> {
|
||||
|
||||
shared.seen_runtime_events.clear();
|
||||
|
||||
init_message.finalized_block_hashes =
|
||||
finalized_ev.finalized_block_hashes.clone();
|
||||
init_message
|
||||
.finalized_block_hashes
|
||||
.clone_from(&finalized_ev.finalized_block_hashes);
|
||||
|
||||
if let Some(runtime_ev) = newest_runtime {
|
||||
init_message.finalized_block_runtime = Some(runtime_ev);
|
||||
@@ -379,6 +380,103 @@ struct SubscriberDetails<Hash: BlockHash> {
|
||||
waker: Option<Waker>,
|
||||
}
|
||||
|
||||
/// A stream that subscribes to finalized blocks
|
||||
/// and indicates whether a block was missed if was restarted.
|
||||
#[derive(Debug)]
|
||||
pub struct FollowStreamFinalizedHeads<Hash: BlockHash, F> {
|
||||
stream: FollowStreamDriverSubscription<Hash>,
|
||||
sub_id: Option<String>,
|
||||
last_seen_block: Option<BlockRef<Hash>>,
|
||||
f: F,
|
||||
is_done: bool,
|
||||
}
|
||||
|
||||
impl<Hash: BlockHash, F> Unpin for FollowStreamFinalizedHeads<Hash, F> {}
|
||||
|
||||
impl<Hash, F> FollowStreamFinalizedHeads<Hash, F>
|
||||
where
|
||||
Hash: BlockHash,
|
||||
F: Fn(FollowEvent<BlockRef<Hash>>) -> Vec<BlockRef<Hash>>,
|
||||
{
|
||||
pub fn new(stream: FollowStreamDriverSubscription<Hash>, f: F) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
sub_id: None,
|
||||
last_seen_block: None,
|
||||
f,
|
||||
is_done: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Hash, F> Stream for FollowStreamFinalizedHeads<Hash, F>
|
||||
where
|
||||
Hash: BlockHash,
|
||||
F: Fn(FollowEvent<BlockRef<Hash>>) -> Vec<BlockRef<Hash>>,
|
||||
{
|
||||
type Item = Result<(String, Vec<BlockRef<Hash>>), Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if self.is_done {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
loop {
|
||||
let Some(ev) = futures::ready!(self.stream.poll_next_unpin(cx)) else {
|
||||
self.is_done = true;
|
||||
return Poll::Ready(None);
|
||||
};
|
||||
|
||||
let block_refs = match ev {
|
||||
FollowStreamMsg::Ready(sub_id) => {
|
||||
self.sub_id = Some(sub_id);
|
||||
continue;
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::Finalized(finalized)) => {
|
||||
self.last_seen_block = finalized.finalized_block_hashes.last().cloned();
|
||||
|
||||
(self.f)(FollowEvent::Finalized(finalized))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::Initialized(mut init)) => {
|
||||
let prev = self.last_seen_block.take();
|
||||
self.last_seen_block = init.finalized_block_hashes.last().cloned();
|
||||
|
||||
if let Some(p) = prev {
|
||||
let Some(pos) = init
|
||||
.finalized_block_hashes
|
||||
.iter()
|
||||
.position(|b| b.hash() == p.hash())
|
||||
else {
|
||||
return Poll::Ready(Some(Err(RpcError::DisconnectedWillReconnect(
|
||||
"Missed at least one block when the connection was lost".to_owned(),
|
||||
)
|
||||
.into())));
|
||||
};
|
||||
|
||||
// If we got older blocks than `prev`, we need to remove them
|
||||
// because they should already have been sent at this point.
|
||||
init.finalized_block_hashes.drain(0..=pos);
|
||||
}
|
||||
|
||||
(self.f)(FollowEvent::Initialized(init))
|
||||
}
|
||||
FollowStreamMsg::Event(ev) => (self.f)(ev),
|
||||
};
|
||||
|
||||
if block_refs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sub_id = self
|
||||
.sub_id
|
||||
.clone()
|
||||
.expect("Ready is always emitted before any other event");
|
||||
|
||||
return Poll::Ready(Some(Ok((sub_id, block_refs))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils {
|
||||
use super::super::follow_stream_unpin::test_utils::test_unpin_stream_getter;
|
||||
@@ -401,6 +499,9 @@ mod test_utils {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use futures::TryStreamExt;
|
||||
use sp_core::H256;
|
||||
|
||||
use super::super::follow_stream::test_utils::{
|
||||
ev_best_block, ev_finalized, ev_initialized, ev_new_block,
|
||||
};
|
||||
@@ -544,4 +645,101 @@ mod test {
|
||||
];
|
||||
assert_eq!(evs, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_finalized_blocks_restart_works() {
|
||||
let mut driver = test_follow_stream_driver_getter(
|
||||
|| {
|
||||
[
|
||||
Ok(ev_initialized(0)),
|
||||
Ok(ev_new_block(0, 1)),
|
||||
Ok(ev_best_block(1)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Ok(FollowEvent::Stop),
|
||||
Ok(ev_initialized(1)),
|
||||
Ok(ev_finalized([2], [])),
|
||||
Err(Error::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
10,
|
||||
);
|
||||
|
||||
let handle = driver.handle();
|
||||
|
||||
tokio::spawn(async move { while driver.next().await.is_some() {} });
|
||||
|
||||
let f = |ev| match ev {
|
||||
FollowEvent::Finalized(ev) => ev.finalized_block_hashes,
|
||||
FollowEvent::Initialized(ev) => ev.finalized_block_hashes,
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let stream = FollowStreamFinalizedHeads::new(handle.subscribe(), f);
|
||||
let evs: Vec<_> = stream.try_collect().await.unwrap();
|
||||
|
||||
let expected = vec![
|
||||
(
|
||||
"sub_id_0".to_string(),
|
||||
vec![BlockRef::new(H256::from_low_u64_le(0))],
|
||||
),
|
||||
(
|
||||
"sub_id_0".to_string(),
|
||||
vec![BlockRef::new(H256::from_low_u64_le(1))],
|
||||
),
|
||||
(
|
||||
"sub_id_5".to_string(),
|
||||
vec![BlockRef::new(H256::from_low_u64_le(2))],
|
||||
),
|
||||
];
|
||||
assert_eq!(evs, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_finalized_blocks_restart_with_missed_blocks() {
|
||||
let mut driver = test_follow_stream_driver_getter(
|
||||
|| {
|
||||
[
|
||||
Ok(ev_initialized(0)),
|
||||
Ok(FollowEvent::Stop),
|
||||
// Emulate that we missed some blocks.
|
||||
Ok(ev_initialized(13)),
|
||||
Ok(ev_finalized([14], [])),
|
||||
Err(Error::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
10,
|
||||
);
|
||||
|
||||
let handle = driver.handle();
|
||||
|
||||
tokio::spawn(async move { while driver.next().await.is_some() {} });
|
||||
|
||||
let f = |ev| match ev {
|
||||
FollowEvent::Finalized(ev) => ev.finalized_block_hashes,
|
||||
FollowEvent::Initialized(ev) => ev.finalized_block_hashes,
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let evs: Vec<_> = FollowStreamFinalizedHeads::new(handle.subscribe(), f)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
evs[0].as_ref().unwrap(),
|
||||
&(
|
||||
"sub_id_0".to_string(),
|
||||
vec![BlockRef::new(H256::from_low_u64_le(0))]
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
matches!(&evs[1], Err(Error::Rpc(RpcError::DisconnectedWillReconnect(e))) if e.contains("Missed at least one block when the connection was lost"))
|
||||
);
|
||||
assert_eq!(
|
||||
evs[2].as_ref().unwrap(),
|
||||
&(
|
||||
"sub_id_2".to_string(),
|
||||
vec![BlockRef::new(H256::from_low_u64_le(14))]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -474,7 +474,7 @@ pub(super) mod test_utils {
|
||||
|
||||
pub type UnpinRx<Hash> = std::sync::mpsc::Receiver<(Hash, Arc<str>)>;
|
||||
|
||||
/// Get a `FolowStreamUnpin` from an iterator over events.
|
||||
/// Get a [`FollowStreamUnpin`] from an iterator over events.
|
||||
pub fn test_unpin_stream_getter<Hash, F, I>(
|
||||
events: F,
|
||||
max_life: usize,
|
||||
|
||||
+178
-144
@@ -18,21 +18,22 @@ mod storage_items;
|
||||
|
||||
pub mod rpc_methods;
|
||||
|
||||
use self::follow_stream_driver::FollowStreamFinalizedHeads;
|
||||
use self::rpc_methods::{
|
||||
FollowEvent, MethodResponse, RuntimeEvent, StorageQuery, StorageQueryType, StorageResultType,
|
||||
};
|
||||
use crate::backend::{
|
||||
rpc::RpcClient, Backend, BlockRef, BlockRefT, RuntimeVersion, StorageResponse, StreamOf,
|
||||
StreamOfResults, TransactionStatus,
|
||||
rpc::RpcClient, utils::retry, Backend, BlockRef, BlockRefT, RuntimeVersion, StorageResponse,
|
||||
StreamOf, StreamOfResults, TransactionStatus,
|
||||
};
|
||||
use crate::config::BlockHash;
|
||||
use crate::error::{Error, RpcError};
|
||||
use crate::Config;
|
||||
use async_trait::async_trait;
|
||||
use follow_stream_driver::{FollowStreamDriver, FollowStreamDriverHandle};
|
||||
use futures::future::Either;
|
||||
use futures::{Stream, StreamExt};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use storage_items::StorageItems;
|
||||
|
||||
@@ -136,43 +137,50 @@ impl<T: Config> UnstableBackend<T> {
|
||||
}
|
||||
|
||||
/// Stream block headers based on the provided filter fn
|
||||
async fn stream_headers<F, I>(
|
||||
async fn stream_headers<F>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error>
|
||||
where
|
||||
F: Fn(FollowEvent<follow_stream_unpin::BlockRef<T::Hash>>) -> I + Copy + Send + 'static,
|
||||
I: IntoIterator<Item = follow_stream_unpin::BlockRef<T::Hash>> + Send + 'static,
|
||||
<I as IntoIterator>::IntoIter: Send,
|
||||
F: Fn(
|
||||
FollowEvent<follow_stream_unpin::BlockRef<T::Hash>>,
|
||||
) -> Vec<follow_stream_unpin::BlockRef<T::Hash>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
let sub_id = get_subscription_id(&self.follow_handle).await?;
|
||||
let sub_id = Arc::new(sub_id);
|
||||
let methods = self.methods.clone();
|
||||
let headers = self.follow_handle.subscribe().events().flat_map(move |ev| {
|
||||
let sub_id = sub_id.clone();
|
||||
let methods = methods.clone();
|
||||
|
||||
let block_refs = f(ev).into_iter();
|
||||
|
||||
futures::stream::iter(block_refs).filter_map(move |block_ref| {
|
||||
let sub_id = sub_id.clone();
|
||||
let headers =
|
||||
FollowStreamFinalizedHeads::new(self.follow_handle.subscribe(), f).flat_map(move |r| {
|
||||
let methods = methods.clone();
|
||||
|
||||
async move {
|
||||
let res = methods
|
||||
.chainhead_v1_header(&sub_id, block_ref.hash())
|
||||
.await
|
||||
.transpose()?;
|
||||
let (sub_id, block_refs) = match r {
|
||||
Ok(ev) => ev,
|
||||
Err(e) => return Either::Left(futures::stream::once(async { Err(e) })),
|
||||
};
|
||||
|
||||
let header = match res {
|
||||
Ok(header) => header,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
Either::Right(
|
||||
futures::stream::iter(block_refs).filter_map(move |block_ref| {
|
||||
let methods = methods.clone();
|
||||
let sub_id = sub_id.clone();
|
||||
|
||||
Some(Ok((header, block_ref.into())))
|
||||
}
|
||||
})
|
||||
});
|
||||
async move {
|
||||
let res = methods
|
||||
.chainhead_v1_header(&sub_id, block_ref.hash())
|
||||
.await
|
||||
.transpose()?;
|
||||
|
||||
let header = match res {
|
||||
Ok(header) => header,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
|
||||
Some(Ok((header, block_ref.into())))
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(headers)))
|
||||
}
|
||||
@@ -194,31 +202,34 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
|
||||
keys: Vec<Vec<u8>>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<StorageResponse>, Error> {
|
||||
let queries = keys.iter().map(|key| StorageQuery {
|
||||
key: &**key,
|
||||
query_type: StorageQueryType::Value,
|
||||
});
|
||||
retry(|| async {
|
||||
let queries = keys.iter().map(|key| StorageQuery {
|
||||
key: &**key,
|
||||
query_type: StorageQueryType::Value,
|
||||
});
|
||||
|
||||
let storage_items =
|
||||
StorageItems::from_methods(queries, at, &self.follow_handle, self.methods.clone())
|
||||
.await?;
|
||||
let storage_items =
|
||||
StorageItems::from_methods(queries, at, &self.follow_handle, self.methods.clone())
|
||||
.await?;
|
||||
|
||||
let storage_result_stream = storage_items.filter_map(|val| async move {
|
||||
let val = match val {
|
||||
Ok(val) => val,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
let stream = storage_items.filter_map(|val| async move {
|
||||
let val = match val {
|
||||
Ok(val) => val,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
|
||||
let StorageResultType::Value(result) = val.result else {
|
||||
return None;
|
||||
};
|
||||
Some(Ok(StorageResponse {
|
||||
key: val.key.0,
|
||||
value: result.0,
|
||||
}))
|
||||
});
|
||||
let StorageResultType::Value(result) = val.result else {
|
||||
return None;
|
||||
};
|
||||
Some(Ok(StorageResponse {
|
||||
key: val.key.0,
|
||||
value: result.0,
|
||||
}))
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(storage_result_stream)))
|
||||
Ok(StreamOf(Box::pin(stream)))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_keys(
|
||||
@@ -226,22 +237,25 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
|
||||
key: Vec<u8>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<Vec<u8>>, Error> {
|
||||
// Ask for hashes, and then just ignore them and return the keys that come back.
|
||||
let query = StorageQuery {
|
||||
key: &*key,
|
||||
query_type: StorageQueryType::DescendantsHashes,
|
||||
};
|
||||
retry(|| async {
|
||||
// Ask for hashes, and then just ignore them and return the keys that come back.
|
||||
let query = StorageQuery {
|
||||
key: &*key,
|
||||
query_type: StorageQueryType::DescendantsHashes,
|
||||
};
|
||||
|
||||
let storage_items = StorageItems::from_methods(
|
||||
std::iter::once(query),
|
||||
at,
|
||||
&self.follow_handle,
|
||||
self.methods.clone(),
|
||||
)
|
||||
.await?;
|
||||
let storage_items = StorageItems::from_methods(
|
||||
std::iter::once(query),
|
||||
at,
|
||||
&self.follow_handle,
|
||||
self.methods.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let storage_result_stream = storage_items.map(|val| val.map(|v| v.key.0));
|
||||
Ok(StreamOf(Box::pin(storage_result_stream)))
|
||||
let storage_result_stream = storage_items.map(|val| val.map(|v| v.key.0));
|
||||
Ok(StreamOf(Box::pin(storage_result_stream)))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_values(
|
||||
@@ -249,72 +263,81 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
|
||||
key: Vec<u8>,
|
||||
at: T::Hash,
|
||||
) -> Result<StreamOfResults<StorageResponse>, Error> {
|
||||
let query = StorageQuery {
|
||||
key: &*key,
|
||||
query_type: StorageQueryType::DescendantsValues,
|
||||
};
|
||||
|
||||
let storage_items = StorageItems::from_methods(
|
||||
std::iter::once(query),
|
||||
at,
|
||||
&self.follow_handle,
|
||||
self.methods.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let storage_result_stream = storage_items.filter_map(|val| async move {
|
||||
let val = match val {
|
||||
Ok(val) => val,
|
||||
Err(e) => return Some(Err(e)),
|
||||
retry(|| async {
|
||||
let query = StorageQuery {
|
||||
key: &*key,
|
||||
query_type: StorageQueryType::DescendantsValues,
|
||||
};
|
||||
|
||||
let StorageResultType::Value(result) = val.result else {
|
||||
return None;
|
||||
};
|
||||
Some(Ok(StorageResponse {
|
||||
key: val.key.0,
|
||||
value: result.0,
|
||||
}))
|
||||
});
|
||||
let storage_items = StorageItems::from_methods(
|
||||
std::iter::once(query),
|
||||
at,
|
||||
&self.follow_handle,
|
||||
self.methods.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(StreamOf(Box::pin(storage_result_stream)))
|
||||
let storage_result_stream = storage_items.filter_map(|val| async move {
|
||||
let val = match val {
|
||||
Ok(val) => val,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
|
||||
let StorageResultType::Value(result) = val.result else {
|
||||
return None;
|
||||
};
|
||||
Some(Ok(StorageResponse {
|
||||
key: val.key.0,
|
||||
value: result.0,
|
||||
}))
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(storage_result_stream)))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn genesis_hash(&self) -> Result<T::Hash, Error> {
|
||||
self.methods.chainspec_v1_genesis_hash().await
|
||||
retry(|| self.methods.chainspec_v1_genesis_hash()).await
|
||||
}
|
||||
|
||||
async fn block_header(&self, at: T::Hash) -> Result<Option<T::Header>, Error> {
|
||||
let sub_id = get_subscription_id(&self.follow_handle).await?;
|
||||
self.methods.chainhead_v1_header(&sub_id, at).await
|
||||
retry(|| async {
|
||||
let sub_id = get_subscription_id(&self.follow_handle).await?;
|
||||
self.methods.chainhead_v1_header(&sub_id, at).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_body(&self, at: T::Hash) -> Result<Option<Vec<Vec<u8>>>, Error> {
|
||||
let sub_id = get_subscription_id(&self.follow_handle).await?;
|
||||
retry(|| async {
|
||||
let sub_id = get_subscription_id(&self.follow_handle).await?;
|
||||
|
||||
// Subscribe to the body response and get our operationId back.
|
||||
let follow_events = self.follow_handle.subscribe().events();
|
||||
let status = self.methods.chainhead_v1_body(&sub_id, at).await?;
|
||||
let operation_id = match status {
|
||||
MethodResponse::LimitReached => {
|
||||
return Err(RpcError::request_rejected("limit reached").into())
|
||||
}
|
||||
MethodResponse::Started(s) => s.operation_id,
|
||||
};
|
||||
|
||||
// Wait for the response to come back with the correct operationId.
|
||||
let mut exts_stream = follow_events.filter_map(|ev| {
|
||||
let FollowEvent::OperationBodyDone(body) = ev else {
|
||||
return std::future::ready(None);
|
||||
// Subscribe to the body response and get our operationId back.
|
||||
let follow_events = self.follow_handle.subscribe().events();
|
||||
let status = self.methods.chainhead_v1_body(&sub_id, at).await?;
|
||||
let operation_id = match status {
|
||||
MethodResponse::LimitReached => {
|
||||
return Err(RpcError::request_rejected("limit reached").into())
|
||||
}
|
||||
MethodResponse::Started(s) => s.operation_id,
|
||||
};
|
||||
if body.operation_id != operation_id {
|
||||
return std::future::ready(None);
|
||||
}
|
||||
let exts: Vec<_> = body.value.into_iter().map(|ext| ext.0).collect();
|
||||
std::future::ready(Some(exts))
|
||||
});
|
||||
|
||||
Ok(exts_stream.next().await)
|
||||
// Wait for the response to come back with the correct operationId.
|
||||
let mut exts_stream = follow_events.filter_map(|ev| {
|
||||
let FollowEvent::OperationBodyDone(body) = ev else {
|
||||
return std::future::ready(None);
|
||||
};
|
||||
if body.operation_id != operation_id {
|
||||
return std::future::ready(None);
|
||||
}
|
||||
let exts: Vec<_> = body.value.into_iter().map(|ext| ext.0).collect();
|
||||
std::future::ready(Some(exts))
|
||||
});
|
||||
|
||||
Ok(exts_stream.next().await)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn latest_finalized_block_ref(&self) -> Result<BlockRef<T::Hash>, Error> {
|
||||
@@ -423,12 +446,16 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
|
||||
std::future::ready(Some(Ok(runtime_version)))
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(runtime_stream)))
|
||||
Ok(StreamOf::new(Box::pin(runtime_stream)))
|
||||
}
|
||||
|
||||
async fn stream_all_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
|
||||
// TODO: https://github.com/paritytech/subxt/issues/1568
|
||||
//
|
||||
// It's possible that blocks may be silently missed if
|
||||
// a reconnection occurs because it's restarted by the unstable backend.
|
||||
self.stream_headers(|ev| match ev {
|
||||
FollowEvent::Initialized(init) => init.finalized_block_hashes,
|
||||
FollowEvent::NewBlock(ev) => {
|
||||
@@ -442,6 +469,10 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
|
||||
async fn stream_best_block_headers(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
|
||||
// TODO: https://github.com/paritytech/subxt/issues/1568
|
||||
//
|
||||
// It's possible that blocks may be silently missed if
|
||||
// a reconnection occurs because it's restarted by the unstable backend.
|
||||
self.stream_headers(|ev| match ev {
|
||||
FollowEvent::Initialized(init) => init.finalized_block_hashes,
|
||||
FollowEvent::BestBlockChanged(ev) => vec![ev.best_block_hash],
|
||||
@@ -638,37 +669,40 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: T::Hash,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let sub_id = get_subscription_id(&self.follow_handle).await?;
|
||||
retry(|| async {
|
||||
let sub_id = get_subscription_id(&self.follow_handle).await?;
|
||||
|
||||
// Subscribe to the body response and get our operationId back.
|
||||
let follow_events = self.follow_handle.subscribe().events();
|
||||
let call_parameters = call_parameters.unwrap_or(&[]);
|
||||
let status = self
|
||||
.methods
|
||||
.chainhead_v1_call(&sub_id, at, method, call_parameters)
|
||||
.await?;
|
||||
let operation_id = match status {
|
||||
MethodResponse::LimitReached => {
|
||||
return Err(RpcError::request_rejected("limit reached").into())
|
||||
}
|
||||
MethodResponse::Started(s) => s.operation_id,
|
||||
};
|
||||
|
||||
// Wait for the response to come back with the correct operationId.
|
||||
let mut call_data_stream = follow_events.filter_map(|ev| {
|
||||
let FollowEvent::OperationCallDone(body) = ev else {
|
||||
return std::future::ready(None);
|
||||
// Subscribe to the body response and get our operationId back.
|
||||
let follow_events = self.follow_handle.subscribe().events();
|
||||
let call_parameters = call_parameters.unwrap_or(&[]);
|
||||
let status = self
|
||||
.methods
|
||||
.chainhead_v1_call(&sub_id, at, method, call_parameters)
|
||||
.await?;
|
||||
let operation_id = match status {
|
||||
MethodResponse::LimitReached => {
|
||||
return Err(RpcError::request_rejected("limit reached").into())
|
||||
}
|
||||
MethodResponse::Started(s) => s.operation_id,
|
||||
};
|
||||
if body.operation_id != operation_id {
|
||||
return std::future::ready(None);
|
||||
}
|
||||
std::future::ready(Some(body.output.0))
|
||||
});
|
||||
|
||||
call_data_stream
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| RpcError::SubscriptionDropped.into())
|
||||
// Wait for the response to come back with the correct operationId.
|
||||
let mut call_data_stream = follow_events.filter_map(|ev| {
|
||||
let FollowEvent::OperationCallDone(body) = ev else {
|
||||
return std::future::ready(None);
|
||||
};
|
||||
if body.operation_id != operation_id {
|
||||
return std::future::ready(None);
|
||||
}
|
||||
std::future::ready(Some(body.output.0))
|
||||
});
|
||||
|
||||
call_data_stream
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| RpcError::SubscriptionDropped.into())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,9 +77,7 @@ impl<T: Config> UnstableRpcMethods<T> {
|
||||
"chainHead_v1_continue",
|
||||
rpc_params![follow_subscription, operation_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Stops an operation started with `chainHead_v1_body`, `chainHead_v1_call`, or
|
||||
@@ -97,9 +95,7 @@ impl<T: Config> UnstableRpcMethods<T> {
|
||||
"chainHead_v1_stopOperation",
|
||||
rpc_params![follow_subscription, operation_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Call the `chainHead_v1_body` method and return an operation ID to obtain the block's body.
|
||||
@@ -222,9 +218,7 @@ impl<T: Config> UnstableRpcMethods<T> {
|
||||
) -> Result<(), Error> {
|
||||
self.client
|
||||
.request("chainHead_v1_unpin", rpc_params![subscription_id, hash])
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Return the genesis hash.
|
||||
|
||||
@@ -111,6 +111,11 @@ impl<T: Config> Stream for StorageItems<T> {
|
||||
return Poll::Pending;
|
||||
}
|
||||
Poll::Ready(Err(e)) => {
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
self.continue_fut = Some((self.continue_call)());
|
||||
continue;
|
||||
}
|
||||
|
||||
self.done = true;
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
//! RPC utils.
|
||||
|
||||
use super::{StreamOf, StreamOfResults};
|
||||
use crate::error::Error;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::{FutureExt, Stream, StreamExt};
|
||||
use std::{future::Future, pin::Pin, task::Poll};
|
||||
|
||||
/// Resubscribe callback.
|
||||
type ResubscribeGetter<T> = Box<dyn FnMut() -> ResubscribeFuture<T> + Send>;
|
||||
|
||||
/// Future that resolves to a subscription stream.
|
||||
type ResubscribeFuture<T> = Pin<Box<dyn Future<Output = Result<StreamOfResults<T>, Error>> + Send>>;
|
||||
|
||||
pub(crate) enum PendingOrStream<T> {
|
||||
Pending(BoxFuture<'static, Result<StreamOfResults<T>, Error>>),
|
||||
Stream(StreamOfResults<T>),
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for PendingOrStream<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PendingOrStream::Pending(_) => write!(f, "Pending"),
|
||||
PendingOrStream::Stream(_) => write!(f, "Stream"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retry subscription.
|
||||
struct RetrySubscription<T> {
|
||||
resubscribe: ResubscribeGetter<T>,
|
||||
state: Option<PendingOrStream<T>>,
|
||||
}
|
||||
|
||||
impl<T> std::marker::Unpin for RetrySubscription<T> {}
|
||||
|
||||
impl<T> Stream for RetrySubscription<T> {
|
||||
type Item = Result<T, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
let Some(mut this) = self.state.take() else {
|
||||
return Poll::Ready(None);
|
||||
};
|
||||
|
||||
match this {
|
||||
PendingOrStream::Stream(ref mut s) => match s.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Err(err))) => {
|
||||
if err.is_disconnected_will_reconnect() {
|
||||
self.state = Some(PendingOrStream::Pending((self.resubscribe)()));
|
||||
}
|
||||
return Poll::Ready(Some(Err(err)));
|
||||
}
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Ready(Some(Ok(val))) => {
|
||||
self.state = Some(this);
|
||||
return Poll::Ready(Some(Ok(val)));
|
||||
}
|
||||
Poll::Pending => {
|
||||
self.state = Some(this);
|
||||
return Poll::Pending;
|
||||
}
|
||||
},
|
||||
PendingOrStream::Pending(mut fut) => match fut.poll_unpin(cx) {
|
||||
Poll::Ready(Ok(stream)) => {
|
||||
self.state = Some(PendingOrStream::Stream(stream));
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Err(err)) => {
|
||||
if err.is_disconnected_will_reconnect() {
|
||||
self.state = Some(PendingOrStream::Pending((self.resubscribe)()));
|
||||
}
|
||||
return Poll::Ready(Some(Err(err)));
|
||||
}
|
||||
Poll::Pending => {
|
||||
self.state = Some(PendingOrStream::Pending(fut));
|
||||
return Poll::Pending;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retry a future until it doesn't return a disconnected error.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use subxt::backend::utils::retry;
|
||||
///
|
||||
/// async fn some_future() -> Result<(), subxt::error::Error> {
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let result = retry(|| some_future()).await;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn retry<T, F, R>(mut retry_future: F) -> Result<R, Error>
|
||||
where
|
||||
F: FnMut() -> T,
|
||||
T: Future<Output = Result<R, Error>>,
|
||||
{
|
||||
const REJECTED_MAX_RETRIES: usize = 10;
|
||||
let mut rejected_retries = 0;
|
||||
|
||||
loop {
|
||||
match retry_future().await {
|
||||
Ok(v) => return Ok(v),
|
||||
Err(e) => {
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: https://github.com/paritytech/subxt/issues/1567
|
||||
// This is a hack because if a reconnection occurs
|
||||
// the order of pending calls is not guaranteed.
|
||||
//
|
||||
// Such that it's possible the a pending future completes
|
||||
// before `chainHead_follow` is established with fresh
|
||||
// subscription id.
|
||||
//
|
||||
if e.is_rejected() && rejected_retries < REJECTED_MAX_RETRIES {
|
||||
rejected_retries += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a retry stream that will resubscribe on disconnect.
|
||||
///
|
||||
/// It's important to note that this function is intended to work only for stateless subscriptions.
|
||||
/// If the subscription takes input or modifies state, this function should not be used.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use subxt::backend::{utils::retry_stream, StreamOf};
|
||||
/// use futures::future::FutureExt;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// retry_stream(|| {
|
||||
/// // This needs to return a stream of results but if you are using
|
||||
/// // the subxt backend already it will return StreamOf so you can just
|
||||
/// // return it directly in the async block below.
|
||||
/// async move { Ok(StreamOf::new(Box::pin(futures::stream::iter([Ok(2)])))) }.boxed()
|
||||
/// }).await;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn retry_stream<F, R>(sub_stream: F) -> Result<StreamOfResults<R>, Error>
|
||||
where
|
||||
F: FnMut() -> ResubscribeFuture<R> + Send + 'static + Clone,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let stream = retry(sub_stream.clone()).await?;
|
||||
|
||||
let resubscribe = Box::new(move || {
|
||||
let sub_stream = sub_stream.clone();
|
||||
async move { retry(sub_stream).await }.boxed()
|
||||
});
|
||||
|
||||
// The extra Box is to encapsulate the retry subscription type
|
||||
Ok(StreamOf::new(Box::pin(RetrySubscription {
|
||||
state: Some(PendingOrStream::Stream(stream)),
|
||||
resubscribe,
|
||||
})))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::backend::StreamOf;
|
||||
|
||||
fn disconnect_err() -> Error {
|
||||
Error::Rpc(crate::error::RpcError::DisconnectedWillReconnect(
|
||||
String::new(),
|
||||
))
|
||||
}
|
||||
|
||||
fn custom_err() -> Error {
|
||||
Error::Other(String::new())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_stream_works() {
|
||||
let retry_stream = retry_stream(|| {
|
||||
async {
|
||||
Ok(StreamOf::new(Box::pin(futures::stream::iter([
|
||||
Ok(1),
|
||||
Ok(2),
|
||||
Ok(3),
|
||||
Err(disconnect_err()),
|
||||
]))))
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = retry_stream
|
||||
.take(5)
|
||||
.collect::<Vec<Result<usize, Error>>>()
|
||||
.await;
|
||||
|
||||
assert!(matches!(result[0], Ok(r) if r == 1));
|
||||
assert!(matches!(result[1], Ok(r) if r == 2));
|
||||
assert!(matches!(result[2], Ok(r) if r == 3));
|
||||
assert!(matches!(result[3], Err(ref e) if e.is_disconnected_will_reconnect()));
|
||||
assert!(matches!(result[4], Ok(r) if r == 1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_sub_works() {
|
||||
let stream = futures::stream::iter([Ok(1), Err(disconnect_err())]);
|
||||
|
||||
let resubscribe = Box::new(move || {
|
||||
async move { Ok(StreamOf::new(Box::pin(futures::stream::iter([Ok(2)])))) }.boxed()
|
||||
});
|
||||
|
||||
let retry_stream = RetrySubscription {
|
||||
state: Some(PendingOrStream::Stream(StreamOf::new(Box::pin(stream)))),
|
||||
resubscribe,
|
||||
};
|
||||
|
||||
let result: Vec<_> = retry_stream.collect().await;
|
||||
|
||||
assert!(matches!(result[0], Ok(r) if r == 1));
|
||||
assert!(matches!(result[1], Err(ref e) if e.is_disconnected_will_reconnect()));
|
||||
assert!(matches!(result[2], Ok(r) if r == 2));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_sub_err_terminates_stream() {
|
||||
let stream = futures::stream::iter([Ok(1)]);
|
||||
let resubscribe = Box::new(move || async move { Err(custom_err()) }.boxed());
|
||||
|
||||
let retry_stream = RetrySubscription {
|
||||
state: Some(PendingOrStream::Stream(StreamOf::new(Box::pin(stream)))),
|
||||
resubscribe,
|
||||
};
|
||||
|
||||
assert_eq!(retry_stream.count().await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_sub_resubscribe_err() {
|
||||
let stream = futures::stream::iter([Ok(1), Err(disconnect_err())]);
|
||||
let resubscribe = Box::new(move || async move { Err(custom_err()) }.boxed());
|
||||
|
||||
let retry_stream = RetrySubscription {
|
||||
state: Some(PendingOrStream::Stream(StreamOf::new(Box::pin(stream)))),
|
||||
resubscribe,
|
||||
};
|
||||
|
||||
let result: Vec<_> = retry_stream.collect().await;
|
||||
|
||||
assert!(matches!(result[0], Ok(r) if r == 1));
|
||||
assert!(matches!(result[1], Err(ref e) if e.is_disconnected_will_reconnect()));
|
||||
assert!(matches!(result[2], Err(ref e) if matches!(e, Error::Other(_))));
|
||||
}
|
||||
}
|
||||
@@ -95,8 +95,8 @@ where
|
||||
{
|
||||
let client = self.client.clone();
|
||||
header_sub_fut_to_block_sub(self.clone(), async move {
|
||||
let sub = client.backend().stream_all_block_headers().await?;
|
||||
BlockStreamRes::Ok(sub)
|
||||
let stream = client.backend().stream_all_block_headers().await?;
|
||||
BlockStreamRes::Ok(stream)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -112,8 +112,8 @@ where
|
||||
{
|
||||
let client = self.client.clone();
|
||||
header_sub_fut_to_block_sub(self.clone(), async move {
|
||||
let sub = client.backend().stream_best_block_headers().await?;
|
||||
BlockStreamRes::Ok(sub)
|
||||
let stream = client.backend().stream_best_block_headers().await?;
|
||||
BlockStreamRes::Ok(stream)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -126,8 +126,8 @@ where
|
||||
{
|
||||
let client = self.client.clone();
|
||||
header_sub_fut_to_block_sub(self.clone(), async move {
|
||||
let sub = client.backend().stream_finalized_block_headers().await?;
|
||||
BlockStreamRes::Ok(sub)
|
||||
let stream = client.backend().stream_finalized_block_headers().await?;
|
||||
BlockStreamRes::Ok(stream)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +159,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::hash()`].
|
||||
pub fn hash(&self) -> T::Hash {
|
||||
self.inner.hash()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::is_signed()`].
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.inner.is_signed()
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
//! Using the [`#[subxt]`](crate::subxt) macro carries some downsides:
|
||||
//!
|
||||
//! - Using it to generate an interface will have a small impact on compile times (though much less of
|
||||
//! one if you only need a few pallets).
|
||||
//! one if you only need a few pallets).
|
||||
//! - IDE support for autocompletion and documentation when using the macro interface can be poor.
|
||||
//! - It's impossible to manually look at the generated code to understand and debug things.
|
||||
//!
|
||||
|
||||
@@ -196,6 +196,14 @@
|
||||
//! This example doesn't wait for the transaction to be included in a block; it just submits it and
|
||||
//! hopes for the best!
|
||||
//!
|
||||
//! ### Boxing transaction payloads
|
||||
//!
|
||||
//! Transaction payloads can be boxed so that they all share a common type and can be stored together.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/tx_boxed.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Custom handling of transaction status updates
|
||||
//!
|
||||
//! If you'd like more control or visibility over exactly which status updates are being emitted for
|
||||
@@ -205,6 +213,15 @@
|
||||
#![doc = include_str!("../../../examples/tx_status_stream.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Signing transactions externally
|
||||
//!
|
||||
//! Subxt also allows you to get hold of the signer payload and hand that off to something else to be
|
||||
//! signed. The signature can then be provided back to Subxt to build the final transaction to submit:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/tx_partial.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! Take a look at the API docs for [`crate::tx::TxProgress`], [`crate::tx::TxStatus`] and
|
||||
//! [`crate::tx::TxInBlock`] for more options.
|
||||
//!
|
||||
@@ -432,9 +432,8 @@ impl<T: Config> ClientRuntimeUpdater<T> {
|
||||
/// Instead that's up to the user of this API to decide when to update and
|
||||
/// to perform the actual updating.
|
||||
pub async fn runtime_updates(&self) -> Result<RuntimeUpdaterStream<T>, Error> {
|
||||
let stream = self.0.backend().stream_runtime_version().await?;
|
||||
Ok(RuntimeUpdaterStream {
|
||||
stream,
|
||||
stream: self.0.backend().stream_runtime_version().await?,
|
||||
client: self.0.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,6 +125,11 @@ impl Error {
|
||||
pub fn is_disconnected_will_reconnect(&self) -> bool {
|
||||
matches!(self, Error::Rpc(RpcError::DisconnectedWillReconnect(_)))
|
||||
}
|
||||
|
||||
/// Checks whether the error was caused by a RPC request being rejected.
|
||||
pub fn is_rejected(&self) -> bool {
|
||||
matches!(self, Error::Rpc(RpcError::RequestRejected(_)))
|
||||
}
|
||||
}
|
||||
|
||||
/// An RPC error. Since we are generic over the RPC client that is used,
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ macro_rules! cfg_jsonrpsee_web {
|
||||
macro_rules! cfg_reconnecting_rpc_client {
|
||||
($($item:item)*) => {
|
||||
$(
|
||||
#[cfg(feature = "unstable-reconnecting-rpc-client")]
|
||||
#[cfg(all(feature = "unstable-reconnecting-rpc-client", any(feature = "native", feature = "web")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-reconnecting-rpc-client")))]
|
||||
$item
|
||||
)*
|
||||
|
||||
@@ -599,11 +599,10 @@ pub enum TransactionInvalid {
|
||||
///
|
||||
/// # Possible causes
|
||||
///
|
||||
/// For `FRAME`-based runtimes this would be caused by `current block number
|
||||
/// For `FRAME`-based runtimes this would be caused by `current block number`
|
||||
/// - Era::birth block number > BlockHashCount`. (e.g. in Polkadot `BlockHashCount` = 2400, so
|
||||
/// a
|
||||
/// transaction with birth block number 1337 would be valid up until block number 1337 + 2400,
|
||||
/// after which point the transaction would be considered to have an ancient birth block.)
|
||||
/// a transaction with birth block number 1337 would be valid up until block number 1337 + 2400,
|
||||
/// after which point the transaction would be considered to have an ancient birth block.)
|
||||
AncientBirthBlock,
|
||||
/// The transaction would exhaust the resources of current block.
|
||||
///
|
||||
|
||||
@@ -11,11 +11,11 @@ use crate::{
|
||||
client::OnlineClientT,
|
||||
error::{DispatchError, Error, RpcError, TransactionError},
|
||||
events::EventsClient,
|
||||
utils::strip_compact_prefix,
|
||||
Config,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use futures::{Stream, StreamExt};
|
||||
use subxt_core::utils::strip_compact_prefix;
|
||||
|
||||
/// This struct represents a subscription to the progress of some transaction.
|
||||
pub struct TxProgress<T: Config, C> {
|
||||
|
||||
@@ -36,7 +36,7 @@ serde = { workspace = true }
|
||||
scale-info = { workspace = true, features = ["bit-vec"] }
|
||||
sp-core = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
subxt = { workspace = true, features = ["unstable-metadata", "native", "jsonrpsee", "substrate-compat"] }
|
||||
subxt = { workspace = true, features = ["unstable-metadata", "native", "jsonrpsee", "substrate-compat", "unstable-reconnecting-rpc-client"] }
|
||||
subxt-signer = { workspace = true, features = ["default"] }
|
||||
subxt-codegen = { workspace = true }
|
||||
subxt-metadata = { workspace = true }
|
||||
@@ -49,4 +49,4 @@ substrate-runner = { workspace = true }
|
||||
subxt-test-macro = { path = "subxt-test-macro" }
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2.0"
|
||||
cfg_aliases = "0.2.1"
|
||||
@@ -226,23 +226,39 @@ async fn fetch_block_and_decode_extrinsic_details() {
|
||||
.map(|res| res.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// All blocks contain a timestamp; check this first:
|
||||
let timestamp = block_extrinsics.first().unwrap();
|
||||
timestamp.as_root_extrinsic::<node_runtime::Call>().unwrap();
|
||||
timestamp
|
||||
.as_extrinsic::<node_runtime::timestamp::calls::types::Set>()
|
||||
.unwrap();
|
||||
assert!(!timestamp.is_signed());
|
||||
let mut balance = None;
|
||||
let mut timestamp = None;
|
||||
|
||||
// Next we expect our transfer:
|
||||
let tx = block_extrinsics.get(1).unwrap();
|
||||
tx.as_root_extrinsic::<node_runtime::Call>().unwrap();
|
||||
let ext = tx
|
||||
.as_extrinsic::<node_runtime::balances::calls::types::TransferAllowDeath>()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(ext.value, 10_000);
|
||||
assert!(tx.is_signed());
|
||||
for tx in block_extrinsics {
|
||||
tx.as_root_extrinsic::<node_runtime::Call>().unwrap();
|
||||
|
||||
if let Some(ext) = tx
|
||||
.as_extrinsic::<node_runtime::timestamp::calls::types::Set>()
|
||||
.unwrap()
|
||||
{
|
||||
timestamp = Some((ext, tx.is_signed()));
|
||||
}
|
||||
|
||||
if let Some(ext) = tx
|
||||
.as_extrinsic::<node_runtime::balances::calls::types::TransferAllowDeath>()
|
||||
.unwrap()
|
||||
{
|
||||
balance = Some((ext, tx.is_signed()));
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we found the timestamp
|
||||
{
|
||||
let (_, is_signed) = timestamp.expect("Timestamp not found");
|
||||
assert!(!is_signed);
|
||||
}
|
||||
|
||||
// Check that we found the balance transfer
|
||||
{
|
||||
let (tx, is_signed) = balance.expect("Balance transfer not found");
|
||||
assert_eq!(tx.value, 10_000);
|
||||
assert!(is_signed);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(fullclient)]
|
||||
@@ -329,6 +345,7 @@ async fn decode_signed_extensions_from_blocks() {
|
||||
"CheckNonce",
|
||||
"CheckWeight",
|
||||
"ChargeAssetTxPayment",
|
||||
"CheckMetadataHash",
|
||||
];
|
||||
|
||||
assert_eq!(extensions1.iter().count(), expected_signed_extensions.len());
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
subxt_test, test_context,
|
||||
subxt_test, test_context, test_context_reconnecting_rpc_client,
|
||||
utils::{node_runtime, wait_for_blocks},
|
||||
};
|
||||
use codec::{Decode, Encode};
|
||||
@@ -42,7 +44,7 @@ async fn storage_fetch_raw_keys() {
|
||||
.count()
|
||||
.await;
|
||||
|
||||
assert_eq!(len, 13)
|
||||
assert_eq!(len, 14)
|
||||
}
|
||||
|
||||
#[cfg(fullclient)]
|
||||
@@ -67,7 +69,7 @@ async fn storage_iter() {
|
||||
.count()
|
||||
.await;
|
||||
|
||||
assert_eq!(len, 13);
|
||||
assert_eq!(len, 14);
|
||||
}
|
||||
|
||||
#[cfg(fullclient)]
|
||||
@@ -359,7 +361,7 @@ pub struct InclusionFee {
|
||||
/// - `targeted_fee_adjustment`: This is a multiplier that can tune the final fee based on the
|
||||
/// congestion of the network.
|
||||
/// - `weight_fee`: This amount is computed based on the weight of the transaction. Weight
|
||||
/// accounts for the execution time of a transaction.
|
||||
/// accounts for the execution time of a transaction.
|
||||
///
|
||||
/// adjusted_weight_fee = targeted_fee_adjustment * weight_fee
|
||||
pub adjusted_weight_fee: u128,
|
||||
@@ -409,3 +411,56 @@ async fn partial_fee_estimate_correct() {
|
||||
// Both methods should yield the same fee
|
||||
assert_eq!(partial_fee_1, partial_fee_2);
|
||||
}
|
||||
|
||||
#[subxt_test]
|
||||
async fn legacy_and_unstable_block_subscription_reconnect() {
|
||||
let ctx = test_context_reconnecting_rpc_client().await;
|
||||
let api = ctx.unstable_client().await;
|
||||
let unstable_client_blocks = move |num: usize| {
|
||||
let api = api.clone();
|
||||
async move {
|
||||
let mut missed_blocks = false;
|
||||
(api.blocks()
|
||||
.subscribe_finalized()
|
||||
.await
|
||||
.unwrap()
|
||||
// Ignore `disconnected events`.
|
||||
// This will be emitted by the legacy backend for every reconnection.
|
||||
.filter(|item| {
|
||||
let disconnected = match item {
|
||||
Ok(_) => false,
|
||||
Err(e) => {
|
||||
if matches!(e, Error::Rpc(subxt::error::RpcError::DisconnectedWillReconnect(e)) if e.contains("Missed at least one block when the connection was lost")) {
|
||||
missed_blocks = true;
|
||||
}
|
||||
e.is_disconnected_will_reconnect()
|
||||
}
|
||||
};
|
||||
|
||||
futures::future::ready(!disconnected)
|
||||
})
|
||||
.take(num)
|
||||
.map(|x| x.unwrap().hash().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.await, missed_blocks)
|
||||
}
|
||||
};
|
||||
|
||||
let (blocks, _) = unstable_client_blocks(3).await;
|
||||
let blocks: HashSet<String> = HashSet::from_iter(blocks.into_iter());
|
||||
|
||||
assert!(blocks.len() == 3);
|
||||
|
||||
let ctx = ctx.restart().await;
|
||||
|
||||
// Make client aware that connection was dropped and force them to reconnect
|
||||
let _ = ctx.unstable_client().await.backend().genesis_hash().await;
|
||||
|
||||
let (unstable_blocks, blocks_missed) = unstable_client_blocks(6).await;
|
||||
|
||||
if !blocks_missed {
|
||||
let unstable_blocks: HashSet<String> = HashSet::from_iter(unstable_blocks.into_iter());
|
||||
let intersection = unstable_blocks.intersection(&blocks).count();
|
||||
assert!(intersection >= 3, "intersections size is {}", intersection);
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,12 @@ async fn chainhead_v1_follow() {
|
||||
let event = blocks.next().await.unwrap().unwrap();
|
||||
// The initialized event should contain the finalized block hash.
|
||||
let finalized_block_hash = legacy_rpc.chain_get_finalized_head().await.unwrap();
|
||||
assert_eq!(
|
||||
assert_matches!(
|
||||
event,
|
||||
FollowEvent::Initialized(Initialized {
|
||||
finalized_block_hashes: vec![finalized_block_hash],
|
||||
finalized_block_runtime: None,
|
||||
})
|
||||
FollowEvent::Initialized(Initialized { finalized_block_hashes, finalized_block_runtime }) => {
|
||||
assert!(finalized_block_hashes.contains(&finalized_block_hash));
|
||||
assert!(finalized_block_runtime.is_none());
|
||||
}
|
||||
);
|
||||
|
||||
// Expect subscription to produce runtime versions.
|
||||
@@ -50,7 +50,7 @@ async fn chainhead_v1_follow() {
|
||||
assert_matches!(
|
||||
event,
|
||||
FollowEvent::Initialized(init) => {
|
||||
assert_eq!(init.finalized_block_hashes, vec![finalized_block_hash]);
|
||||
assert!(init.finalized_block_hashes.contains(&finalized_block_hash));
|
||||
if let Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec })) = init.finalized_block_runtime {
|
||||
assert_eq!(spec.spec_version, runtime_version.spec_version);
|
||||
assert_eq!(spec.transaction_version, runtime_version.transaction_version);
|
||||
@@ -326,18 +326,18 @@ async fn transaction_v1_broadcast() {
|
||||
let api = ctx.client();
|
||||
let rpc = ctx.unstable_rpc_methods().await;
|
||||
|
||||
let tx = node_runtime::tx()
|
||||
let tx_payload = node_runtime::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(bob_address.clone(), 10_001);
|
||||
|
||||
let tx_bytes = ctx
|
||||
let tx = ctx
|
||||
.client()
|
||||
.tx()
|
||||
.create_signed_offline(&tx, &dev::alice(), Default::default())
|
||||
.unwrap()
|
||||
.into_encoded();
|
||||
.create_signed_offline(&tx_payload, &dev::alice(), Default::default())
|
||||
.unwrap();
|
||||
|
||||
let tx_hash = <SubstrateConfig as subxt::Config>::Hasher::hash(&tx_bytes[2..]);
|
||||
let tx_hash = tx.hash();
|
||||
let tx_bytes = tx.into_encoded();
|
||||
|
||||
// Subscribe to finalized blocks.
|
||||
let mut finalized_sub = api.blocks().subscribe_finalized().await.unwrap();
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -29,26 +29,47 @@
|
||||
|
||||
use crate::utils::node_runtime;
|
||||
use codec::Compact;
|
||||
use futures::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use subxt::backend::rpc::RpcClient;
|
||||
use subxt::backend::unstable::UnstableBackend;
|
||||
use subxt::{client::OnlineClient, config::PolkadotConfig, lightclient::LightClient};
|
||||
use subxt_metadata::Metadata;
|
||||
|
||||
type Client = OnlineClient<PolkadotConfig>;
|
||||
|
||||
/// The Polkadot chainspec.
|
||||
const POLKADOT_SPEC: &str = include_str!("../../../../artifacts/demo_chain_specs/polkadot.json");
|
||||
|
||||
// Check that we can subscribe to non-finalized blocks.
|
||||
async fn non_finalized_headers_subscription(api: &Client) -> Result<(), subxt::Error> {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
tracing::trace!("Check non_finalized_headers_subscription");
|
||||
let mut sub = api.blocks().subscribe_best().await?;
|
||||
|
||||
let _block = sub.next().await.unwrap()?;
|
||||
tracing::trace!("First block took {:?}", now.elapsed());
|
||||
|
||||
let _block = sub.next().await.unwrap()?;
|
||||
tracing::trace!("Second block took {:?}", now.elapsed());
|
||||
|
||||
let _block = sub.next().await.unwrap()?;
|
||||
tracing::trace!("Third block took {:?}", now.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Check that we can subscribe to finalized blocks.
|
||||
async fn finalized_headers_subscription(api: &Client) -> Result<(), subxt::Error> {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
tracing::trace!("Check finalized_headers_subscription");
|
||||
|
||||
let mut sub = api.blocks().subscribe_finalized().await?;
|
||||
let header = sub.next().await.unwrap()?;
|
||||
tracing::trace!("First block took {:?}", now.elapsed());
|
||||
|
||||
let finalized_hash = api
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
@@ -56,20 +77,34 @@ async fn finalized_headers_subscription(api: &Client) -> Result<(), subxt::Error
|
||||
.unwrap()
|
||||
.hash();
|
||||
|
||||
tracing::trace!(
|
||||
"Finalized hash: {:?} took {:?}",
|
||||
finalized_hash,
|
||||
now.elapsed()
|
||||
);
|
||||
|
||||
assert_eq!(header.hash(), finalized_hash);
|
||||
tracing::trace!("Check progress {:?}", now.elapsed());
|
||||
|
||||
let _block = sub.next().await.unwrap()?;
|
||||
tracing::trace!("Second block took {:?}", now.elapsed());
|
||||
let _block = sub.next().await.unwrap()?;
|
||||
tracing::trace!("Third block took {:?}", now.elapsed());
|
||||
let _block = sub.next().await.unwrap()?;
|
||||
tracing::trace!("Fourth block took {:?}\n", now.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Check that we can subscribe to non-finalized blocks.
|
||||
async fn runtime_api_call(api: &Client) -> Result<(), subxt::Error> {
|
||||
let now = std::time::Instant::now();
|
||||
tracing::trace!("Check runtime_api_call");
|
||||
|
||||
let mut sub = api.blocks().subscribe_best().await?;
|
||||
|
||||
let block = sub.next().await.unwrap()?;
|
||||
tracing::trace!("First block took {:?}", now.elapsed());
|
||||
let rt = block.runtime_api().await?;
|
||||
|
||||
// get metadata via state_call. if it decodes ok, it's probably all good.
|
||||
@@ -77,11 +112,16 @@ async fn runtime_api_call(api: &Client) -> Result<(), subxt::Error> {
|
||||
.call_raw::<(Compact<u32>, Metadata)>("Metadata_metadata", None)
|
||||
.await?;
|
||||
|
||||
tracing::trace!("Made runtime API call in {:?}\n", now.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Lookup for the `Timestamp::now` plain storage entry.
|
||||
async fn storage_plain_lookup(api: &Client) -> Result<(), subxt::Error> {
|
||||
let now = std::time::Instant::now();
|
||||
tracing::trace!("Check storage_plain_lookup");
|
||||
|
||||
let addr = node_runtime::storage().timestamp().now();
|
||||
let entry = api
|
||||
.storage()
|
||||
@@ -90,6 +130,8 @@ async fn storage_plain_lookup(api: &Client) -> Result<(), subxt::Error> {
|
||||
.fetch_or_default(&addr)
|
||||
.await?;
|
||||
|
||||
tracing::trace!("Storage lookup took {:?}\n", now.elapsed());
|
||||
|
||||
assert!(entry > 0);
|
||||
|
||||
Ok(())
|
||||
@@ -97,30 +139,75 @@ async fn storage_plain_lookup(api: &Client) -> Result<(), subxt::Error> {
|
||||
|
||||
// Make a dynamic constant query for `System::BlockLenght`.
|
||||
async fn dynamic_constant_query(api: &Client) -> Result<(), subxt::Error> {
|
||||
let now = std::time::Instant::now();
|
||||
tracing::trace!("Check dynamic_constant_query");
|
||||
|
||||
let constant_query = subxt::dynamic::constant("System", "BlockLength");
|
||||
let _value = api.constants().at(&constant_query)?;
|
||||
|
||||
tracing::trace!("Dynamic constant query took {:?}\n", now.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Fetch a few all events from the latest block and decode them dynamically.
|
||||
async fn dynamic_events(api: &Client) -> Result<(), subxt::Error> {
|
||||
let now = std::time::Instant::now();
|
||||
tracing::trace!("Check dynamic_events");
|
||||
|
||||
let events = api.events().at_latest().await?;
|
||||
|
||||
for event in events.iter() {
|
||||
let _event = event?;
|
||||
|
||||
tracing::trace!("Event decoding took {:?}", now.elapsed());
|
||||
}
|
||||
|
||||
tracing::trace!("Dynamic events took {:?}\n", now.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn light_client_testing() -> Result<(), subxt::Error> {
|
||||
let chainspec = subxt::utils::fetch_chainspec_from_rpc_node("wss://rpc.polkadot.io:443")
|
||||
.await
|
||||
.unwrap();
|
||||
let (_lc, rpc) = LightClient::relay_chain(chainspec.get())?;
|
||||
let api = Client::from_rpc_client(rpc).await?;
|
||||
async fn run_test(backend: BackendType) -> Result<(), subxt::Error> {
|
||||
// Note: This code fetches the chainspec from the Polkadot public RPC node.
|
||||
// This is not recommended for production use, as it may be slow and unreliable.
|
||||
// However, this can come in handy for testing purposes.
|
||||
//
|
||||
// let chainspec = subxt::utils::fetch_chainspec_from_rpc_node("wss://rpc.polkadot.io:443")
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let chain_config = chainspec.get();
|
||||
|
||||
tracing::trace!("Init light clinet");
|
||||
let now = std::time::Instant::now();
|
||||
let (_lc, rpc) = LightClient::relay_chain(POLKADOT_SPEC)?;
|
||||
|
||||
let api = match backend {
|
||||
BackendType::Unstable => {
|
||||
let (backend, mut driver) = UnstableBackend::builder().build(RpcClient::new(rpc));
|
||||
tokio::spawn(async move {
|
||||
while let Some(val) = driver.next().await {
|
||||
if let Err(e) = val {
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
tracing::info!(
|
||||
"The RPC connection was lost and we may have missed a few blocks"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::error!("Error driving unstable backend: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
let api: OnlineClient<PolkadotConfig> =
|
||||
OnlineClient::from_backend(Arc::new(backend)).await?;
|
||||
api
|
||||
}
|
||||
|
||||
BackendType::Legacy => Client::from_rpc_client(rpc).await?,
|
||||
};
|
||||
|
||||
tracing::trace!("Light client initialization took {:?}", now.elapsed());
|
||||
|
||||
non_finalized_headers_subscription(&api).await?;
|
||||
finalized_headers_subscription(&api).await?;
|
||||
@@ -129,5 +216,25 @@ async fn light_client_testing() -> Result<(), subxt::Error> {
|
||||
dynamic_constant_query(&api).await?;
|
||||
dynamic_events(&api).await?;
|
||||
|
||||
tracing::trace!("Light complete testing took {:?}", now.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Backend type for light client testing.
|
||||
enum BackendType {
|
||||
/// Use the unstable backend (ie chainHead).
|
||||
Unstable,
|
||||
/// Use the legacy backend.
|
||||
Legacy,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn light_client_testing() -> Result<(), subxt::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Run light client test with both backends.
|
||||
run_test(BackendType::Unstable).await?;
|
||||
run_test(BackendType::Legacy).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -7,17 +7,20 @@ pub(crate) use crate::{node_runtime, utils::TestNodeProcess};
|
||||
use subxt::client::OnlineClient;
|
||||
use subxt::SubstrateConfig;
|
||||
|
||||
use super::node_proc::RpcClientKind;
|
||||
|
||||
/// `substrate-node` should be installed on the $PATH. We fall back
|
||||
/// to also checking for an older `substrate` binary.
|
||||
const SUBSTRATE_NODE_PATHS: &str = "substrate-node,substrate";
|
||||
|
||||
pub async fn test_context_with(authority: String) -> TestContext {
|
||||
pub async fn test_context_with(authority: String, rpc_client_kind: RpcClientKind) -> TestContext {
|
||||
let paths =
|
||||
std::env::var("SUBSTRATE_NODE_PATH").unwrap_or_else(|_| SUBSTRATE_NODE_PATHS.to_string());
|
||||
let paths: Vec<_> = paths.split(',').map(|p| p.trim()).collect();
|
||||
|
||||
let mut proc = TestContext::build(&paths);
|
||||
proc.with_authority(authority);
|
||||
proc.with_rpc_client_kind(rpc_client_kind);
|
||||
proc.spawn::<SubstrateConfig>().await.unwrap()
|
||||
}
|
||||
|
||||
@@ -28,5 +31,9 @@ pub type TestContext = TestNodeProcess<SubstrateConfig>;
|
||||
pub type TestClient = OnlineClient<SubstrateConfig>;
|
||||
|
||||
pub async fn test_context() -> TestContext {
|
||||
test_context_with("alice".to_string()).await
|
||||
test_context_with("alice".to_string(), RpcClientKind::Legacy).await
|
||||
}
|
||||
|
||||
pub async fn test_context_reconnecting_rpc_client() -> TestContext {
|
||||
test_context_with("alice".to_string(), RpcClientKind::UnstableReconnecting).await
|
||||
}
|
||||
@@ -5,16 +5,40 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use substrate_runner::SubstrateNode;
|
||||
use subxt::backend::rpc::reconnecting_rpc_client::{ExponentialBackoff, RpcClientBuilder};
|
||||
use subxt::{
|
||||
backend::{legacy, rpc, unstable},
|
||||
Config, OnlineClient,
|
||||
};
|
||||
|
||||
// The URL that we'll connect to for our tests comes from SUBXT_TEXT_HOST env var,
|
||||
// defaulting to localhost if not provided. If the env var is set, we won't spawn
|
||||
// a binary. Note though that some tests expect and modify a fresh state, and so will
|
||||
// fail. Fo a similar reason wyou should also use `--test-threads 1` when running tests
|
||||
// to reduce the number of conflicts between state altering tests.
|
||||
const URL_ENV_VAR: &str = "SUBXT_TEST_URL";
|
||||
fn is_url_provided() -> bool {
|
||||
std::env::var(URL_ENV_VAR).is_ok()
|
||||
}
|
||||
fn get_url(port: Option<u16>) -> String {
|
||||
match (std::env::var(URL_ENV_VAR).ok(), port) {
|
||||
(Some(host), None) => host,
|
||||
(None, Some(port)) => format!("ws://127.0.0.1:{port}"),
|
||||
(Some(_), Some(_)) => {
|
||||
panic!("{URL_ENV_VAR} and port provided: only one or the other should exist")
|
||||
}
|
||||
(None, None) => {
|
||||
panic!("No {URL_ENV_VAR} or port was provided, so we don't know where to connect to")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a local substrate node for testing subxt.
|
||||
pub struct TestNodeProcess<R: Config> {
|
||||
// Keep a handle to the node; once it's dropped the node is killed.
|
||||
proc: SubstrateNode,
|
||||
proc: Option<SubstrateNode>,
|
||||
|
||||
// Lazily construct these when asked for.
|
||||
unstable_client: RefCell<Option<OnlineClient<R>>>,
|
||||
@@ -36,26 +60,29 @@ where
|
||||
TestNodeProcessBuilder::new(paths)
|
||||
}
|
||||
|
||||
pub async fn restart(mut self) -> Self {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Some(ref mut proc) = &mut self.proc {
|
||||
proc.restart().unwrap();
|
||||
}
|
||||
self
|
||||
})
|
||||
.await
|
||||
.expect("to succeed")
|
||||
}
|
||||
|
||||
/// Hand back an RPC client connected to the test node which exposes the legacy RPC methods.
|
||||
pub async fn legacy_rpc_methods(&self) -> legacy::LegacyRpcMethods<R> {
|
||||
let rpc_client = self.rpc_client().await;
|
||||
let rpc_client = self.rpc_client.clone();
|
||||
legacy::LegacyRpcMethods::new(rpc_client)
|
||||
}
|
||||
|
||||
/// Hand back an RPC client connected to the test node which exposes the unstable RPC methods.
|
||||
pub async fn unstable_rpc_methods(&self) -> unstable::UnstableRpcMethods<R> {
|
||||
let rpc_client = self.rpc_client().await;
|
||||
let rpc_client = self.rpc_client.clone();
|
||||
unstable::UnstableRpcMethods::new(rpc_client)
|
||||
}
|
||||
|
||||
/// Hand back an RPC client connected to the test node.
|
||||
pub async fn rpc_client(&self) -> rpc::RpcClient {
|
||||
let url = format!("ws://127.0.0.1:{}", self.proc.ws_port());
|
||||
rpc::RpcClient::from_url(url)
|
||||
.await
|
||||
.expect("Unable to connect RPC client to test node")
|
||||
}
|
||||
|
||||
/// Always return a client using the unstable backend.
|
||||
/// Only use for comparing backends; use [`TestNodeProcess::client()`] normally,
|
||||
/// which enables us to run each test against both backends.
|
||||
@@ -87,12 +114,24 @@ where
|
||||
pub fn client(&self) -> OnlineClient<R> {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
/// Returns the rpc client connected to the node
|
||||
pub fn rpc_client(&self) -> rpc::RpcClient {
|
||||
self.rpc_client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Kind of rpc client to use in tests
|
||||
pub enum RpcClientKind {
|
||||
Legacy,
|
||||
UnstableReconnecting,
|
||||
}
|
||||
|
||||
/// Construct a test node process.
|
||||
pub struct TestNodeProcessBuilder {
|
||||
node_paths: Vec<OsString>,
|
||||
authority: Option<String>,
|
||||
rpc_client: RpcClientKind,
|
||||
}
|
||||
|
||||
impl TestNodeProcessBuilder {
|
||||
@@ -110,9 +149,16 @@ impl TestNodeProcessBuilder {
|
||||
Self {
|
||||
node_paths: paths,
|
||||
authority: None,
|
||||
rpc_client: RpcClientKind::Legacy,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the testRunner to use a preferred RpcClient impl, ie Legacy or Unstable
|
||||
pub fn with_rpc_client_kind(&mut self, rpc_client_kind: RpcClientKind) -> &mut Self {
|
||||
self.rpc_client = rpc_client_kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authority dev account for a node in validator mode e.g. --alice.
|
||||
pub fn with_authority(&mut self, account: String) -> &mut Self {
|
||||
self.authority = Some(account);
|
||||
@@ -124,20 +170,26 @@ impl TestNodeProcessBuilder {
|
||||
where
|
||||
R: Config,
|
||||
{
|
||||
let mut node_builder = SubstrateNode::builder();
|
||||
// Only spawn a process if a URL to target wasn't provided as an env var.
|
||||
let proc = if !is_url_provided() {
|
||||
let mut node_builder = SubstrateNode::builder();
|
||||
node_builder.binary_paths(&self.node_paths);
|
||||
|
||||
node_builder.binary_paths(&self.node_paths);
|
||||
if let Some(authority) = &self.authority {
|
||||
node_builder.arg(authority.to_lowercase());
|
||||
}
|
||||
|
||||
if let Some(authority) = &self.authority {
|
||||
node_builder.arg(authority.to_lowercase());
|
||||
Some(node_builder.spawn().map_err(|e| e.to_string())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ws_url = get_url(proc.as_ref().map(|p| p.ws_port()));
|
||||
let rpc_client = match self.rpc_client {
|
||||
RpcClientKind::Legacy => build_rpc_client(&ws_url).await,
|
||||
RpcClientKind::UnstableReconnecting => build_unstable_rpc_client(&ws_url).await,
|
||||
}
|
||||
|
||||
// Spawn the node and retrieve a URL to it:
|
||||
let proc = node_builder.spawn().map_err(|e| e.to_string())?;
|
||||
let ws_url = format!("ws://127.0.0.1:{}", proc.ws_port());
|
||||
let rpc_client = build_rpc_client(&ws_url)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to node at {ws_url}: {e}"))?;
|
||||
.map_err(|e| format!("Failed to connect to node at {ws_url}: {e}"))?;
|
||||
|
||||
// Cache whatever client we build, and None for the other.
|
||||
#[allow(unused_assignments, unused_mut)]
|
||||
@@ -173,13 +225,23 @@ impl TestNodeProcessBuilder {
|
||||
}
|
||||
|
||||
async fn build_rpc_client(ws_url: &str) -> Result<rpc::RpcClient, String> {
|
||||
let rpc_client = rpc::RpcClient::from_url(ws_url)
|
||||
let rpc_client = rpc::RpcClient::from_insecure_url(ws_url)
|
||||
.await
|
||||
.map_err(|e| format!("Cannot construct RPC client: {e}"))?;
|
||||
|
||||
Ok(rpc_client)
|
||||
}
|
||||
|
||||
async fn build_unstable_rpc_client(ws_url: &str) -> Result<rpc::RpcClient, String> {
|
||||
let client = RpcClientBuilder::new()
|
||||
.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
|
||||
.build(ws_url.to_string())
|
||||
.await
|
||||
.map_err(|e| format!("Cannot construct RPC client: {e}"))?;
|
||||
|
||||
Ok(rpc::RpcClient::new(client))
|
||||
}
|
||||
|
||||
async fn build_legacy_client<T: Config>(
|
||||
rpc_client: rpc::RpcClient,
|
||||
) -> Result<OnlineClient<T>, String> {
|
||||
@@ -217,10 +279,18 @@ async fn build_unstable_client<T: Config>(
|
||||
}
|
||||
|
||||
#[cfg(lightclient)]
|
||||
async fn build_light_client<T: Config>(proc: &SubstrateNode) -> Result<OnlineClient<T>, String> {
|
||||
async fn build_light_client<T: Config>(
|
||||
maybe_proc: &Option<SubstrateNode>,
|
||||
) -> Result<OnlineClient<T>, String> {
|
||||
use subxt::lightclient::{ChainConfig, LightClient};
|
||||
|
||||
// RPC endpoint.
|
||||
let proc = if let Some(proc) = maybe_proc {
|
||||
proc
|
||||
} else {
|
||||
return Err("Cannot build light client: no substrate node is running (you can't start a light client when pointing to an external node)".into());
|
||||
};
|
||||
|
||||
// RPC endpoint. Only localhost works.
|
||||
let ws_url = format!("ws://127.0.0.1:{}", proc.ws_port());
|
||||
|
||||
// Wait for a few blocks to be produced using the subxt client.
|
||||
|
||||
@@ -70,16 +70,27 @@ impl SubstrateNodeBuilder {
|
||||
}
|
||||
|
||||
/// Spawn the node, handing back an object which, when dropped, will stop it.
|
||||
pub fn spawn(self) -> Result<SubstrateNode, Error> {
|
||||
pub fn spawn(mut self) -> Result<SubstrateNode, Error> {
|
||||
// Try to spawn the binary at each path, returning the
|
||||
// first "ok" or last error that we encountered.
|
||||
let mut res = Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"No binary path provided",
|
||||
));
|
||||
|
||||
let path = Command::new("mktemp")
|
||||
.arg("-d")
|
||||
.output()
|
||||
.expect("failed to create base dir");
|
||||
let path = String::from_utf8(path.stdout).expect("bad path");
|
||||
let mut bin_path = OsString::new();
|
||||
for binary_path in &self.binary_paths {
|
||||
self.custom_flags
|
||||
.insert("base-path".into(), Some(path.clone().into()));
|
||||
|
||||
res = SubstrateNodeBuilder::try_spawn(binary_path, &self.custom_flags);
|
||||
if res.is_ok() {
|
||||
bin_path.clone_from(binary_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -98,10 +109,13 @@ impl SubstrateNodeBuilder {
|
||||
let p2p_port = p2p_port.ok_or(Error::CouldNotExtractP2pPort)?;
|
||||
|
||||
Ok(SubstrateNode {
|
||||
binary_path: bin_path,
|
||||
custom_flags: self.custom_flags,
|
||||
proc,
|
||||
ws_port,
|
||||
p2p_address,
|
||||
p2p_port,
|
||||
base_path: path,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -131,10 +145,13 @@ impl SubstrateNodeBuilder {
|
||||
}
|
||||
|
||||
pub struct SubstrateNode {
|
||||
binary_path: OsString,
|
||||
custom_flags: HashMap<CowStr, Option<CowStr>>,
|
||||
proc: process::Child,
|
||||
ws_port: u16,
|
||||
p2p_address: String,
|
||||
p2p_port: u32,
|
||||
base_path: String,
|
||||
}
|
||||
|
||||
impl SubstrateNode {
|
||||
@@ -167,11 +184,61 @@ impl SubstrateNode {
|
||||
pub fn kill(&mut self) -> std::io::Result<()> {
|
||||
self.proc.kill()
|
||||
}
|
||||
|
||||
/// restart the node, handing back an object which, when dropped, will stop it.
|
||||
pub fn restart(&mut self) -> Result<(), std::io::Error> {
|
||||
let res: Result<(), io::Error> = self.kill();
|
||||
|
||||
match res {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
self.cleanup();
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
let proc = self.try_spawn()?;
|
||||
|
||||
self.proc = proc;
|
||||
// Wait for RPC port to be logged (it's logged to stderr).
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Attempt to spawn a binary with the path/flags given.
|
||||
fn try_spawn(&mut self) -> Result<Child, std::io::Error> {
|
||||
let mut cmd = Command::new(&self.binary_path);
|
||||
|
||||
cmd.env("RUST_LOG", "info,libp2p_tcp=debug")
|
||||
.stdout(process::Stdio::piped())
|
||||
.stderr(process::Stdio::piped())
|
||||
.arg("--dev");
|
||||
|
||||
for (key, val) in &self.custom_flags {
|
||||
let arg = match val {
|
||||
Some(val) => format!("--{key}={val}"),
|
||||
None => format!("--{key}"),
|
||||
};
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
cmd.arg(format!("--rpc-port={}", self.ws_port));
|
||||
cmd.arg(format!("--port={}", self.p2p_port));
|
||||
cmd.spawn()
|
||||
}
|
||||
|
||||
fn cleanup(&self) {
|
||||
let _ = Command::new("rm")
|
||||
.args(["-rf", &self.base_path])
|
||||
.output()
|
||||
.expect("success");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SubstrateNode {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.kill();
|
||||
self.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,9 +264,10 @@ fn try_find_substrate_port_from_output(
|
||||
.or_else(|| line.rsplit_once("Running JSON-RPC server: addr=127.0.0.1:"))
|
||||
.map(|(_, port_str)| port_str);
|
||||
|
||||
if let Some(line_port) = line_port {
|
||||
// trim non-numeric chars from the end of the port part of the line.
|
||||
let port_str = line_port.trim_end_matches(|b: char| !b.is_ascii_digit());
|
||||
if let Some(ports) = line_port {
|
||||
// If more than one rpc server is started the log will capture multiple ports
|
||||
// such as `addr=127.0.0.1:9944,[::1]:9944`
|
||||
let port_str: String = ports.chars().take_while(|c| c.is_numeric()).collect();
|
||||
|
||||
// expect to have a number here (the chars after '127.0.0.1:') and parse them into a u16.
|
||||
let port_num = port_str
|
||||
|
||||
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user