mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 08:51:09 +00:00
Merge remote-tracking branch 'origin/master' into lexnv/codegen-config
This commit is contained in:
@@ -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**
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
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 one or more lines are too long
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user