fix: Convert vendor/pezkuwi-subxt from submodule to regular directory
This commit is contained in:
+163
@@ -0,0 +1,163 @@
|
||||
[package]
|
||||
name = "pezkuwi-subxt"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = true
|
||||
|
||||
license.workspace = true
|
||||
readme = "../README.md"
|
||||
repository.workspace = true
|
||||
documentation.workspace = true
|
||||
homepage.workspace = true
|
||||
description = "Submit extrinsics (transactions) to a Pezkuwi/Bizinikiwi node via RPC"
|
||||
keywords = ["pezkuwi", "bizinikiwi", "blockchain"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
# For dev and documentation reasons we enable more features than are often desired.
|
||||
# it's recommended to use `--no-default-features` and then select what you need.
|
||||
default = ["jsonrpsee", "native"]
|
||||
|
||||
# Enable this for native (ie non web/wasm builds).
|
||||
# Exactly 1 of "web" and "native" is expected.
|
||||
native = [
|
||||
"pezkuwi-subxt-lightclient?/native",
|
||||
"pezkuwi-subxt-rpcs/native",
|
||||
"tokio-util",
|
||||
"tokio?/sync",
|
||||
"pezsp-crypto-hashing/std",
|
||||
]
|
||||
|
||||
# Enable this for web/wasm builds.
|
||||
# Exactly 1 of "web" and "native" is expected.
|
||||
web = [
|
||||
"pezkuwi-subxt-lightclient?/web",
|
||||
"pezkuwi-subxt-macro/web",
|
||||
"pezkuwi-subxt-rpcs/web",
|
||||
"tokio?/sync",
|
||||
]
|
||||
|
||||
# Feature flag to enable the default future executor.
|
||||
# Technically it's a hack enable to both but simplifies the conditional compilation
|
||||
# and subxt is selecting executor based on the used platform.
|
||||
#
|
||||
# For instance `wasm-bindgen-futures` panics if the platform isn't wasm32 and
|
||||
# similar for tokio that requires a tokio runtime to be initialized.
|
||||
runtime = ["tokio/rt", "wasm-bindgen-futures"]
|
||||
|
||||
# Enable this to use the reconnecting rpc client
|
||||
reconnecting-rpc-client = ["pezkuwi-subxt-rpcs/reconnecting-rpc-client"]
|
||||
|
||||
# Enable this to use jsonrpsee, which enables the jsonrpsee RPC client, and
|
||||
# a couple of util functions which rely on jsonrpsee.
|
||||
jsonrpsee = [
|
||||
"dep:jsonrpsee",
|
||||
"pezkuwi-subxt-rpcs/jsonrpsee",
|
||||
"runtime"
|
||||
]
|
||||
|
||||
# Enable this to fetch and utilize the latest unstable metadata from a node.
|
||||
# The unstable metadata is subject to breaking changes and the subxt might
|
||||
# fail to decode the metadata properly. Use this to experiment with the
|
||||
# latest features exposed by the metadata.
|
||||
unstable-metadata = []
|
||||
|
||||
# Activate this to expose the Light Client functionality.
|
||||
# Note that this feature is experimental and things may break or not work as expected.
|
||||
unstable-light-client = ["pezkuwi-subxt-lightclient", "pezkuwi-subxt-rpcs/unstable-light-client"]
|
||||
|
||||
# Activate this to expose the ability to generate metadata from Wasm runtime files.
|
||||
runtime-wasm-path = ["pezkuwi-subxt-macro/runtime-wasm-path"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] }
|
||||
derive-where = { workspace = true }
|
||||
scale-info = { workspace = true, features = ["default"] }
|
||||
scale-value = { workspace = true, features = ["default"] }
|
||||
scale-bits = { workspace = true, features = ["default"] }
|
||||
scale-decode = { workspace = true, features = ["default"] }
|
||||
scale-encode = { workspace = true, features = ["default"] }
|
||||
futures = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, features = ["default", "raw_value"] }
|
||||
pezsp-crypto-hashing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
frame-metadata = { workspace = true }
|
||||
either = { workspace = true }
|
||||
web-time = { workspace = true }
|
||||
|
||||
# Provides some deserialization, types like U256/H256 and hashing impls like twox/blake256:
|
||||
primitive-types = { workspace = true, features = ["codec", "scale-info", "serde"] }
|
||||
|
||||
# Included if the "jsonrpsee" feature is enabled.
|
||||
jsonrpsee = { workspace = true, optional = true, features = ["jsonrpsee-types"] }
|
||||
|
||||
# Other pezkuwi-subxt crates we depend on.
|
||||
pezkuwi-subxt-macro = { workspace = true }
|
||||
pezkuwi-subxt-core = { workspace = true, features = ["std"] }
|
||||
pezkuwi-subxt-metadata = { workspace = true, features = ["std"] }
|
||||
pezkuwi-subxt-lightclient = { workspace = true, optional = true, default-features = false }
|
||||
pezkuwi-subxt-rpcs = { workspace = true, features = ["subxt"] }
|
||||
|
||||
# For parsing urls to disallow insecure schemes
|
||||
url = { workspace = 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 }
|
||||
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", "sync"] }
|
||||
pezsp-core = { workspace = true, features = ["std"] }
|
||||
pezsp-keyring = { workspace = true, features = ["std"] }
|
||||
pezsp-runtime = { workspace = true, features = ["std"] }
|
||||
assert_matches = { workspace = true }
|
||||
pezkuwi-subxt-signer = { path = "../signer", features = ["unstable-eth"] }
|
||||
pezkuwi-subxt-rpcs = { workspace = true, features = ["subxt", "mock-rpc-client"] }
|
||||
# Tracing subscriber is useful for light-client examples to ensure that
|
||||
# the `bootNodes` and chain spec are configured correctly. If all is fine, then
|
||||
# the light-client will 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 = { workspace = true }
|
||||
hyper = { workspace = true }
|
||||
http-body = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "light_client_basic"
|
||||
path = "examples/light_client_basic.rs"
|
||||
required-features = ["unstable-light-client", "jsonrpsee"]
|
||||
|
||||
[[example]]
|
||||
name = "light_client_local_node"
|
||||
path = "examples/light_client_local_node.rs"
|
||||
required-features = ["unstable-light-client", "jsonrpsee", "native"]
|
||||
|
||||
[[example]]
|
||||
name = "setup_reconnecting_rpc_client"
|
||||
path = "examples/setup_reconnecting_rpc_client.rs"
|
||||
required-features = ["reconnecting-rpc-client"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["default", "unstable-light-client"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.playground]
|
||||
features = ["default", "unstable-light-client"]
|
||||
@@ -0,0 +1,43 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a client that subscribes to blocks of the Polkadot network.
|
||||
let api = OnlineClient::<PolkadotConfig>::from_url("wss://rpc.polkadot.io:443").await?;
|
||||
|
||||
// Subscribe to all finalized blocks:
|
||||
let mut blocks_sub = api.blocks().subscribe_finalized().await?;
|
||||
while let Some(block) = blocks_sub.next().await {
|
||||
let block = block?;
|
||||
let block_number = block.header().number;
|
||||
let block_hash = block.hash();
|
||||
println!("Block #{block_number} ({block_hash})");
|
||||
|
||||
// Decode each signed extrinsic in the block dynamically
|
||||
let extrinsics = block.extrinsics().await?;
|
||||
for ext in extrinsics.iter() {
|
||||
let Some(transaction_extensions) = ext.transaction_extensions() else {
|
||||
continue; // we do not look at inherents in this example
|
||||
};
|
||||
|
||||
// Decode the fields into our dynamic Value type to display:
|
||||
let fields = ext.decode_as_fields::<scale_value::Value>()?;
|
||||
|
||||
println!(" {}/{}", ext.pallet_name(), ext.call_name());
|
||||
println!(" Transaction Extensions:");
|
||||
for signed_ext in transaction_extensions.iter() {
|
||||
// We only want to take a look at these 3 signed extensions, because the others all just have unit fields.
|
||||
if ["CheckMortality", "CheckNonce", "ChargeTransactionPayment"]
|
||||
.contains(&signed_ext.name())
|
||||
{
|
||||
println!(" {}: {}", signed_ext.name(), signed_ext.value()?);
|
||||
}
|
||||
}
|
||||
println!(" Fields:");
|
||||
println!(" {fields}\n");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{
|
||||
OnlineClient, PolkadotConfig,
|
||||
utils::{AccountId32, MultiAddress},
|
||||
};
|
||||
|
||||
use codec::Decode;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
use polkadot::balances::calls::types::TransferKeepAlive;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a client that subscribes to blocks of the Polkadot network.
|
||||
let api = OnlineClient::<PolkadotConfig>::from_url("wss://rpc.polkadot.io:443").await?;
|
||||
|
||||
// Subscribe to all finalized blocks:
|
||||
let mut blocks_sub = api.blocks().subscribe_finalized().await?;
|
||||
|
||||
// For each block, print details about the `TransferKeepAlive` transactions we are interested in.
|
||||
while let Some(block) = blocks_sub.next().await {
|
||||
let block = block?;
|
||||
let block_number = block.header().number;
|
||||
let block_hash = block.hash();
|
||||
println!("Block #{block_number} ({block_hash}):");
|
||||
|
||||
let extrinsics = block.extrinsics().await?;
|
||||
for transfer in extrinsics.find::<TransferKeepAlive>() {
|
||||
let transfer = transfer?;
|
||||
|
||||
let Some(extensions) = transfer.details.transaction_extensions() else {
|
||||
panic!("TransferKeepAlive should be signed")
|
||||
};
|
||||
|
||||
let addr_bytes = transfer
|
||||
.details
|
||||
.address_bytes()
|
||||
.expect("TransferKeepAlive should be signed");
|
||||
let sender = MultiAddress::<AccountId32, ()>::decode(&mut &addr_bytes[..])
|
||||
.expect("Decoding should work");
|
||||
let sender = display_address(&sender);
|
||||
let receiver = display_address(&transfer.value.dest);
|
||||
let value = transfer.value.value;
|
||||
let tip = extensions.tip().expect("Should have tip");
|
||||
let nonce = extensions.nonce().expect("Should have nonce");
|
||||
|
||||
println!(
|
||||
" Transfer of {value} DOT:\n {sender} (Tip: {tip}, Nonce: {nonce}) ---> {receiver}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_address(addr: &MultiAddress<AccountId32, ()>) -> String {
|
||||
if let MultiAddress::Id(id32) = addr {
|
||||
format!("{id32}")
|
||||
} else {
|
||||
"MultiAddress::...".into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
#[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 client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().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 = block?;
|
||||
|
||||
let block_number = block.header().number;
|
||||
let block_hash = block.hash();
|
||||
|
||||
println!("Block #{block_number}:");
|
||||
println!(" Hash: {block_hash}");
|
||||
println!(" Extrinsics:");
|
||||
|
||||
// Log each of the extrinsic with it's associated events:
|
||||
let extrinsics = block.extrinsics().await?;
|
||||
for ext in extrinsics.iter() {
|
||||
let idx = ext.index();
|
||||
let events = ext.events().await?;
|
||||
let bytes_hex = format!("0x{}", hex::encode(ext.bytes()));
|
||||
|
||||
// See the API docs for more ways to decode extrinsics:
|
||||
let decoded_ext = ext.as_root_extrinsic::<polkadot::Call>();
|
||||
|
||||
println!(" Extrinsic #{idx}:");
|
||||
println!(" Bytes: {bytes_hex}");
|
||||
println!(" Decoded: {decoded_ext:?}");
|
||||
|
||||
println!(" Events:");
|
||||
for evt in events.iter() {
|
||||
let evt = evt?;
|
||||
let pallet_name = evt.pallet_name();
|
||||
let event_name = evt.variant_name();
|
||||
let event_values = evt.decode_as_fields::<scale_value::Value>()?;
|
||||
|
||||
println!(" {pallet_name}_{event_name}");
|
||||
println!(" {event_values}");
|
||||
}
|
||||
|
||||
println!(" Transaction Extensions:");
|
||||
if let Some(transaction_extensions) = ext.transaction_extensions() {
|
||||
for transaction_extension in transaction_extensions.iter() {
|
||||
let name = transaction_extension.name();
|
||||
let value = transaction_extension.value()?.to_string();
|
||||
println!(" {name}: {value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::dynamic::Value;
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// We can query a constant by providing a tuple of the pallet and constant name. The return type
|
||||
// will be `Value` if we pass this query:
|
||||
let constant_query = ("System", "BlockLength");
|
||||
let _value = api.constants().at(&constant_query)?;
|
||||
|
||||
// Or we can use the library function to query a constant, which allows us to pass a generic type
|
||||
// that Subxt will attempt to decode the constant into:
|
||||
let constant_query = subxt::dynamic::constant::<Value>("System", "BlockLength");
|
||||
let value = api.constants().at(&constant_query)?;
|
||||
|
||||
// Or we can obtain the bytes for the constant, using either form of query.
|
||||
let bytes = api.constants().bytes_at(&constant_query)?;
|
||||
|
||||
println!("Constant bytes: {:?}", bytes);
|
||||
println!("Constant value: {}", value);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
#[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 client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// A query to obtain some constant:
|
||||
let constant_query = polkadot::constants().system().block_length();
|
||||
|
||||
// Obtain the value:
|
||||
let value = api.constants().at(&constant_query)?;
|
||||
|
||||
// Or obtain the bytes:
|
||||
let bytes = api.constants().bytes_at(&constant_query)?;
|
||||
|
||||
println!("Encoded block length: {bytes:?}");
|
||||
println!("Block length: {value:?}");
|
||||
Ok(())
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
#[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 client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Get events for the latest block:
|
||||
let events = api.events().at_latest().await?;
|
||||
|
||||
// We can dynamically decode events:
|
||||
println!("Dynamic event details:");
|
||||
for event in events.iter() {
|
||||
let event = event?;
|
||||
|
||||
let pallet = event.pallet_name();
|
||||
let variant = event.variant_name();
|
||||
let field_values = event.decode_as_fields::<scale_value::Value>()?;
|
||||
|
||||
println!("{pallet}::{variant}: {field_values}");
|
||||
}
|
||||
|
||||
// Or we can attempt to statically decode them into the root Event type:
|
||||
println!("Static event details:");
|
||||
for event in events.iter() {
|
||||
let event = event?;
|
||||
|
||||
if let Ok(ev) = event.as_root_event::<polkadot::Event>() {
|
||||
println!("{ev:?}");
|
||||
} else {
|
||||
println!("<Cannot decode event>");
|
||||
}
|
||||
}
|
||||
|
||||
// Or we can look for specific events which match our statically defined ones:
|
||||
let transfer_event = events.find_first::<polkadot::balances::events::Transfer>()?;
|
||||
if let Some(ev) = transfer_event {
|
||||
println!(" - Balance transfer success: value: {:?}", ev.amount);
|
||||
} else {
|
||||
println!(" - No balance transfer event found in this block");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#![allow(missing_docs)]
|
||||
use futures::StreamExt;
|
||||
use subxt::{PolkadotConfig, client::OnlineClient, lightclient::LightClient};
|
||||
|
||||
// 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 {}
|
||||
|
||||
const POLKADOT_SPEC: &str = include_str!("../../artifacts/demo_chain_specs/polkadot.json");
|
||||
const ASSET_HUB_SPEC: &str =
|
||||
include_str!("../../artifacts/demo_chain_specs/polkadot_asset_hub.json");
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// The lightclient logs are informative:
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Instantiate a light client with the Polkadot relay chain,
|
||||
// and connect it to Asset Hub, too.
|
||||
let (lightclient, polkadot_rpc) = LightClient::relay_chain(POLKADOT_SPEC)?;
|
||||
let asset_hub_rpc = lightclient.parachain(ASSET_HUB_SPEC)?;
|
||||
|
||||
// Create Subxt clients from these Smoldot backed RPC clients.
|
||||
let polkadot_api = OnlineClient::<PolkadotConfig>::from_rpc_client(polkadot_rpc).await?;
|
||||
let asset_hub_api = OnlineClient::<PolkadotConfig>::from_rpc_client(asset_hub_rpc).await?;
|
||||
|
||||
// Use them!
|
||||
let polkadot_sub = polkadot_api
|
||||
.blocks()
|
||||
.subscribe_finalized()
|
||||
.await?
|
||||
.map(|block| ("Polkadot", block));
|
||||
let parachain_sub = asset_hub_api
|
||||
.blocks()
|
||||
.subscribe_finalized()
|
||||
.await?
|
||||
.map(|block| ("AssetHub", block));
|
||||
|
||||
let mut stream_combinator = futures::stream::select(polkadot_sub, parachain_sub);
|
||||
|
||||
while let Some((chain, block)) = stream_combinator.next().await {
|
||||
let block = block?;
|
||||
println!(" Chain {:?} hash={:?}", chain, block.hash());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::utils::fetch_chainspec_from_rpc_node;
|
||||
use subxt::{
|
||||
PolkadotConfig,
|
||||
client::OnlineClient,
|
||||
lightclient::{ChainConfig, LightClient},
|
||||
};
|
||||
use pezkuwi_subxt_signer::sr25519::dev;
|
||||
|
||||
// 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>> {
|
||||
// The smoldot logs are informative:
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Use a utility function to obtain a chain spec from a locally running node:
|
||||
let chain_spec = fetch_chainspec_from_rpc_node("ws://127.0.0.1:9944").await?;
|
||||
|
||||
// Configure the bootnodes of this chain spec. In this case, because we start one
|
||||
// single node, the bootnodes must be overwritten for the light client to connect
|
||||
// to the local node.
|
||||
//
|
||||
// The `12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp` is the P2P address
|
||||
// from a local polkadot node starting with
|
||||
// `--node-key 0000000000000000000000000000000000000000000000000000000000000001`
|
||||
let chain_config = ChainConfig::chain_spec(chain_spec.get()).set_bootnodes([
|
||||
"/ip4/127.0.0.1/tcp/30333/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp",
|
||||
])?;
|
||||
|
||||
// Start the light client up, establishing a connection to the local node.
|
||||
let (_light_client, chain_rpc) = LightClient::relay_chain(chain_config)?;
|
||||
let api = OnlineClient::<PolkadotConfig>::from_rpc_client(chain_rpc).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);
|
||||
|
||||
// Submit the balance transfer extrinsic from Alice, and wait for it to be successful
|
||||
// and in a finalized block. We get back the extrinsic events if all is well.
|
||||
let from = dev::alice();
|
||||
let events = api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&balance_transfer_tx, &from)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
// Find a Transfer event and print it.
|
||||
let transfer_event = events.find_first::<polkadot::balances::events::Transfer>()?;
|
||||
if let Some(event) = transfer_event {
|
||||
println!("Balance transfer success: {event:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::backend::{legacy::LegacyRpcMethods, rpc::RpcClient};
|
||||
use subxt::config::DefaultExtrinsicParamsBuilder as Params;
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use pezkuwi_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>> {
|
||||
// First, create a raw RPC client:
|
||||
let rpc_client = RpcClient::from_url("ws://127.0.0.1:9944").await?;
|
||||
|
||||
// Use this to construct our RPC methods:
|
||||
let rpc = LegacyRpcMethods::<PolkadotConfig>::new(rpc_client.clone());
|
||||
|
||||
// We can use the same client to drive our full Subxt interface too:
|
||||
let api = OnlineClient::<PolkadotConfig>::from_rpc_client(rpc_client.clone()).await?;
|
||||
|
||||
// Now, we can make some RPC calls using some legacy RPC methods.
|
||||
println!(
|
||||
"📛 System Name: {:?}\n🩺 Health: {:?}\n🖫 Properties: {:?}\n🔗 Chain: {:?}\n",
|
||||
rpc.system_name().await?,
|
||||
rpc.system_health().await?,
|
||||
rpc.system_properties().await?,
|
||||
rpc.system_chain().await?
|
||||
);
|
||||
|
||||
// We can also interleave RPC calls and using the full Subxt client, here to submit multiple
|
||||
// transactions using the legacy `system_account_next_index` RPC call, which returns a nonce
|
||||
// that is adjusted for any transactions already in the pool:
|
||||
|
||||
let alice = dev::alice();
|
||||
let bob = dev::bob();
|
||||
|
||||
loop {
|
||||
let current_nonce = rpc
|
||||
.system_account_next_index(&alice.public_key().into())
|
||||
.await?;
|
||||
|
||||
let ext_params = Params::new().mortal(8).nonce(current_nonce).build();
|
||||
|
||||
let balance_transfer = polkadot::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(bob.public_key().into(), 1_000_000);
|
||||
|
||||
let ext_hash = api
|
||||
.tx()
|
||||
.create_partial_offline(&balance_transfer, ext_params)?
|
||||
.sign(&alice)
|
||||
.submit()
|
||||
.await?;
|
||||
|
||||
println!("Submitted ext {ext_hash} with nonce {current_nonce}");
|
||||
|
||||
// Sleep less than block time, but long enough to ensure
|
||||
// not all transactions end up in the same block.
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::utils::AccountId32;
|
||||
use subxt::{OnlineClient, config::PolkadotConfig};
|
||||
use pezkuwi_subxt_signer::sr25519::dev;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Create a "dynamic" runtime API payload that calls the
|
||||
// `AccountNonceApi_account_nonce` function. We could use the
|
||||
// `scale_value::Value` type as output, and a vec of those as inputs,
|
||||
// but since we know the input + return types we can pass them directly.
|
||||
// There is one input argument, so the inputs are a tuple of one element.
|
||||
let account: AccountId32 = dev::alice().public_key().into();
|
||||
let runtime_api_call =
|
||||
subxt::dynamic::runtime_api_call::<_, u64>("AccountNonceApi", "account_nonce", (account,));
|
||||
|
||||
// Submit the call to get back a result.
|
||||
let nonce = api
|
||||
.runtime_api()
|
||||
.at_latest()
|
||||
.await?
|
||||
.call(runtime_api_call)
|
||||
.await?;
|
||||
|
||||
println!("Account nonce: {:#?}", nonce);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::ext::codec::{Compact, Decode};
|
||||
use subxt::ext::frame_metadata::RuntimeMetadataPrefixed;
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
#[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 client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Use runtime APIs at the latest block:
|
||||
let runtime_apis = api.runtime_api().at_latest().await?;
|
||||
|
||||
// Ask for metadata and decode it:
|
||||
let result_bytes = runtime_apis.call_raw("Metadata_metadata", None).await?;
|
||||
let (_, meta): (Compact<u32>, RuntimeMetadataPrefixed) = Decode::decode(&mut &*result_bytes)?;
|
||||
|
||||
println!("{meta:?}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, config::PolkadotConfig};
|
||||
use pezkuwi_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>> {
|
||||
// Create a client to use:
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Create a runtime API payload that calls into
|
||||
// `AccountNonceApi_account_nonce` function.
|
||||
let account = dev::alice().public_key().into();
|
||||
let runtime_api_call = polkadot::apis().account_nonce_api().account_nonce(account);
|
||||
|
||||
// Submit the call and get back a result.
|
||||
let nonce = api
|
||||
.runtime_api()
|
||||
.at_latest()
|
||||
.await?
|
||||
.call(runtime_api_call)
|
||||
.await;
|
||||
|
||||
println!("AccountNonceApi_account_nonce for Alice: {nonce:?}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#![allow(missing_docs)]
|
||||
use std::{
|
||||
fmt::Write,
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use subxt::{
|
||||
OnlineClient, PolkadotConfig,
|
||||
backend::rpc::{RawRpcFuture, RawRpcSubscription, RawValue, RpcClient, RpcClientT},
|
||||
};
|
||||
|
||||
// A dummy RPC client that doesn't actually handle requests properly
|
||||
// at all, but instead just logs what requests to it were made.
|
||||
struct MyLoggingClient {
|
||||
log: Arc<Mutex<String>>,
|
||||
}
|
||||
|
||||
// We have to implement this fairly low level trait to turn [`MyLoggingClient`]
|
||||
// into an RPC client that we can make use of in Subxt. Here we just log the requests
|
||||
// made but don't forward them to any real node, and instead just return nonsense.
|
||||
impl RpcClientT for MyLoggingClient {
|
||||
fn request_raw<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
) -> RawRpcFuture<'a, Box<RawValue>> {
|
||||
writeln!(
|
||||
self.log.lock().unwrap(),
|
||||
"{method}({})",
|
||||
params.as_ref().map(|p| p.get()).unwrap_or("[]")
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// We've logged the request; just return garbage. Because a boxed future is returned,
|
||||
// you're able to run whatever async code you'd need to actually talk to a node.
|
||||
let res = RawValue::from_string("[]".to_string()).unwrap();
|
||||
Box::pin(std::future::ready(Ok(res)))
|
||||
}
|
||||
|
||||
fn subscribe_raw<'a>(
|
||||
&'a self,
|
||||
sub: &'a str,
|
||||
params: Option<Box<RawValue>>,
|
||||
unsub: &'a str,
|
||||
) -> RawRpcFuture<'a, RawRpcSubscription> {
|
||||
writeln!(
|
||||
self.log.lock().unwrap(),
|
||||
"{sub}({}) (unsub: {unsub})",
|
||||
params.as_ref().map(|p| p.get()).unwrap_or("[]")
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// We've logged the request; just return garbage. Because a boxed future is returned,
|
||||
// and that will return a boxed Stream impl, you have a bunch of flexibility to build
|
||||
// and return whatever type of Stream you see fit.
|
||||
let res = RawValue::from_string("[]".to_string()).unwrap();
|
||||
let stream = futures::stream::once(async move { Ok(res) });
|
||||
let stream: Pin<Box<dyn futures::Stream<Item = _> + Send>> = Box::pin(stream);
|
||||
// This subscription does not provide an ID.
|
||||
Box::pin(std::future::ready(Ok(RawRpcSubscription {
|
||||
stream,
|
||||
id: None,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Instantiate our replacement RPC client.
|
||||
let log = Arc::default();
|
||||
let rpc_client = {
|
||||
let inner = MyLoggingClient {
|
||||
log: Arc::clone(&log),
|
||||
};
|
||||
RpcClient::new(inner)
|
||||
};
|
||||
|
||||
// Pass this into our OnlineClient to instantiate it. This will lead to some
|
||||
// RPC calls being made to fetch chain details/metadata, which will immediately
|
||||
// fail..
|
||||
let _ = OnlineClient::<PolkadotConfig>::from_rpc_client(rpc_client).await;
|
||||
|
||||
// But, we can see that the calls were made via our custom RPC client:
|
||||
println!("Log of calls made:\n\n{}", log.lock().unwrap().as_str());
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::ext::codec::Decode;
|
||||
use subxt::metadata::Metadata;
|
||||
use subxt::utils::H256;
|
||||
use subxt::{OfflineClient, config::PolkadotConfig};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// We need to obtain the following details for an OfflineClient to be instantiated:
|
||||
|
||||
// 1. Genesis hash (RPC call: chain_getBlockHash(0)):
|
||||
let genesis_hash = {
|
||||
let h = "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3";
|
||||
let bytes = hex::decode(h).unwrap();
|
||||
H256::from_slice(&bytes)
|
||||
};
|
||||
|
||||
// 2. A runtime version (system_version constant on a Substrate node has these):
|
||||
let runtime_version = subxt::client::RuntimeVersion {
|
||||
spec_version: 9370,
|
||||
transaction_version: 20,
|
||||
};
|
||||
|
||||
// 3. Metadata (I'll load it from the downloaded metadata, but you can use
|
||||
// `subxt metadata > file.scale` to download it):
|
||||
let metadata = {
|
||||
let bytes = std::fs::read("./artifacts/polkadot_metadata_small.scale").unwrap();
|
||||
Metadata::decode(&mut &*bytes).unwrap()
|
||||
};
|
||||
|
||||
// Create an offline client using the details obtained above:
|
||||
let _api = OfflineClient::<PolkadotConfig>::new(genesis_hash, runtime_version, metadata);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::config::{
|
||||
Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, PolkadotConfig, SubstrateConfig,
|
||||
};
|
||||
use pezkuwi_subxt_signer::sr25519::dev;
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
derive_for_type(
|
||||
path = "staging_xcm::v3::multilocation::MultiLocation",
|
||||
derive = "Clone, codec::Encode",
|
||||
recursive
|
||||
)
|
||||
)]
|
||||
pub mod runtime {}
|
||||
use runtime::runtime_types::staging_xcm::v3::multilocation::MultiLocation;
|
||||
use runtime::runtime_types::xcm::v3::junctions::Junctions;
|
||||
|
||||
// We don't need to construct this at runtime, so an empty enum is appropriate.
|
||||
pub enum AssetHubConfig {}
|
||||
|
||||
impl Config for AssetHubConfig {
|
||||
type AccountId = <SubstrateConfig as Config>::AccountId;
|
||||
type Address = <PolkadotConfig as Config>::Address;
|
||||
type Signature = <SubstrateConfig as Config>::Signature;
|
||||
type Hasher = <SubstrateConfig as Config>::Hasher;
|
||||
type Header = <SubstrateConfig as Config>::Header;
|
||||
type ExtrinsicParams = DefaultExtrinsicParams<AssetHubConfig>;
|
||||
// Here we use the MultiLocation from the metadata as a part of the config:
|
||||
// The `ChargeAssetTxPayment` signed extension that is part of the ExtrinsicParams above, now uses the type:
|
||||
type AssetId = MultiLocation;
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// With the config defined, we can create an extrinsic with subxt:
|
||||
let client = subxt::OnlineClient::<AssetHubConfig>::new().await.unwrap();
|
||||
let tx_payload = runtime::tx().system().remark(b"Hello".to_vec());
|
||||
|
||||
// Build extrinsic params using an asset at this location as a tip:
|
||||
let location: MultiLocation = MultiLocation {
|
||||
parents: 3,
|
||||
interior: Junctions::Here,
|
||||
};
|
||||
let tx_config = DefaultExtrinsicParamsBuilder::<AssetHubConfig>::new()
|
||||
.tip_of(1234, location)
|
||||
.build();
|
||||
|
||||
// And provide the extrinsic params including the tip when submitting a transaction:
|
||||
let _ = client
|
||||
.tx()
|
||||
.sign_and_submit_then_watch(&tx_payload, &dev::alice(), tx_config)
|
||||
.await;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#![allow(missing_docs)]
|
||||
use codec::Encode;
|
||||
use subxt::client::ClientState;
|
||||
use subxt::config::{
|
||||
Config, ExtrinsicParams, ExtrinsicParamsEncoder, ExtrinsicParamsError, HashFor,
|
||||
transaction_extensions::Params,
|
||||
};
|
||||
use pezkuwi_subxt_signer::sr25519::dev;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale")]
|
||||
pub mod runtime {}
|
||||
|
||||
// We don't need to construct this at runtime,
|
||||
// so an empty enum is appropriate:
|
||||
pub enum CustomConfig {}
|
||||
|
||||
impl Config for CustomConfig {
|
||||
type AccountId = subxt::utils::AccountId32;
|
||||
type Address = subxt::utils::MultiAddress<Self::AccountId, ()>;
|
||||
type Signature = subxt::utils::MultiSignature;
|
||||
type Hasher = subxt::config::substrate::BlakeTwo256;
|
||||
type Header = subxt::config::substrate::SubstrateHeader<u32, Self::Hasher>;
|
||||
type ExtrinsicParams = CustomExtrinsicParams<Self>;
|
||||
type AssetId = u32;
|
||||
}
|
||||
|
||||
// This represents some arbitrary (and nonsensical) custom parameters that
|
||||
// will be attached to transaction extra and additional payloads:
|
||||
pub struct CustomExtrinsicParams<T: Config> {
|
||||
genesis_hash: HashFor<T>,
|
||||
tip: u128,
|
||||
foo: bool,
|
||||
}
|
||||
|
||||
// We can provide a "pretty" interface to allow users to provide these:
|
||||
#[derive(Default)]
|
||||
pub struct CustomExtrinsicParamsBuilder {
|
||||
tip: u128,
|
||||
foo: bool,
|
||||
}
|
||||
|
||||
impl CustomExtrinsicParamsBuilder {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
pub fn tip(mut self, value: u128) -> Self {
|
||||
self.tip = value;
|
||||
self
|
||||
}
|
||||
pub fn enable_foo(mut self) -> Self {
|
||||
self.foo = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for CustomExtrinsicParamsBuilder {}
|
||||
|
||||
// Describe how to fetch and then encode the params:
|
||||
impl<T: Config> ExtrinsicParams<T> for CustomExtrinsicParams<T> {
|
||||
type Params = CustomExtrinsicParamsBuilder;
|
||||
|
||||
// Gather together all of the params we will need to encode:
|
||||
fn new(client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(Self {
|
||||
genesis_hash: client.genesis_hash,
|
||||
tip: params.tip,
|
||||
foo: params.foo,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Encode the relevant params when asked:
|
||||
impl<T: Config> ExtrinsicParamsEncoder for CustomExtrinsicParams<T> {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
(self.tip, self.foo).encode_to(v);
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
self.genesis_hash.encode_to(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// With the config defined, it can be handed to Subxt as follows:
|
||||
let client = subxt::OnlineClient::<CustomConfig>::new().await.unwrap();
|
||||
|
||||
let tx_payload = runtime::tx().system().remark(b"Hello".to_vec());
|
||||
|
||||
// Build your custom "Params":
|
||||
let tx_config = CustomExtrinsicParamsBuilder::new().tip(1234).enable_foo();
|
||||
|
||||
// And provide them when submitting a transaction:
|
||||
let _ = client
|
||||
.tx()
|
||||
.sign_and_submit_then_watch(&tx_payload, &dev::alice(), tx_config)
|
||||
.await;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#![allow(missing_docs)]
|
||||
use codec::Encode;
|
||||
use scale_encode::EncodeAsType;
|
||||
use scale_info::PortableRegistry;
|
||||
use subxt::client::ClientState;
|
||||
use subxt::config::transaction_extensions;
|
||||
use subxt::config::{
|
||||
Config, DefaultExtrinsicParamsBuilder, ExtrinsicParams, ExtrinsicParamsEncoder,
|
||||
ExtrinsicParamsError,
|
||||
};
|
||||
use pezkuwi_subxt_signer::sr25519::dev;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod runtime {}
|
||||
|
||||
// We don't need to construct this at runtime,
|
||||
// so an empty enum is appropriate:
|
||||
#[derive(EncodeAsType)]
|
||||
pub enum CustomConfig {}
|
||||
|
||||
impl Config for CustomConfig {
|
||||
type AccountId = subxt::utils::AccountId32;
|
||||
type Address = subxt::utils::MultiAddress<Self::AccountId, ()>;
|
||||
type Signature = subxt::utils::MultiSignature;
|
||||
type Hasher = subxt::config::substrate::BlakeTwo256;
|
||||
type Header = subxt::config::substrate::SubstrateHeader<u32, Self::Hasher>;
|
||||
type ExtrinsicParams = transaction_extensions::AnyOf<
|
||||
Self,
|
||||
(
|
||||
// Load in the existing signed extensions we're interested in
|
||||
// (if the extension isn't actually needed it'll just be ignored):
|
||||
transaction_extensions::VerifySignature<Self>,
|
||||
transaction_extensions::CheckSpecVersion,
|
||||
transaction_extensions::CheckTxVersion,
|
||||
transaction_extensions::CheckNonce,
|
||||
transaction_extensions::CheckGenesis<Self>,
|
||||
transaction_extensions::CheckMortality<Self>,
|
||||
transaction_extensions::ChargeAssetTxPayment<Self>,
|
||||
transaction_extensions::ChargeTransactionPayment,
|
||||
transaction_extensions::CheckMetadataHash,
|
||||
// And add a new one of our own:
|
||||
CustomTransactionExtension,
|
||||
),
|
||||
>;
|
||||
type AssetId = u32;
|
||||
}
|
||||
|
||||
// Our custom signed extension doesn't do much:
|
||||
pub struct CustomTransactionExtension;
|
||||
|
||||
// Give the extension a name; this allows `AnyOf` to look it
|
||||
// up in the chain metadata in order to know when and if to use it.
|
||||
impl<T: Config> transaction_extensions::TransactionExtension<T> for CustomTransactionExtension {
|
||||
type Decoded = ();
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CustomTransactionExtension"
|
||||
}
|
||||
}
|
||||
|
||||
// Gather together any params we need for our signed extension, here none.
|
||||
impl<T: Config> ExtrinsicParams<T> for CustomTransactionExtension {
|
||||
type Params = ();
|
||||
|
||||
fn new(_client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CustomTransactionExtension)
|
||||
}
|
||||
}
|
||||
|
||||
// Encode whatever the extension needs to provide when asked:
|
||||
impl ExtrinsicParamsEncoder for CustomTransactionExtension {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
"Hello".encode_to(v);
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
true.encode_to(v)
|
||||
}
|
||||
}
|
||||
|
||||
// When composing a tuple of signed extensions, the user parameters we need must
|
||||
// be able to convert `Into` a tuple of corresponding `Params`. Here, we just
|
||||
// "hijack" the default param builder, but add the `Params` (`()`) for our
|
||||
// new signed extension at the end, to make the types line up. IN reality you may wish
|
||||
// to construct an entirely new interface to provide the relevant `Params`.
|
||||
pub fn custom(
|
||||
params: DefaultExtrinsicParamsBuilder<CustomConfig>,
|
||||
) -> <<CustomConfig as Config>::ExtrinsicParams as ExtrinsicParams<CustomConfig>>::Params {
|
||||
let (a, b, c, d, e, f, g, h, i) = params.build();
|
||||
(a, b, c, d, e, f, g, h, i, ())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// With the config defined, it can be handed to Subxt as follows:
|
||||
let client = subxt::OnlineClient::<CustomConfig>::new().await.unwrap();
|
||||
|
||||
let tx_payload = runtime::tx().system().remark(b"Hello".to_vec());
|
||||
|
||||
// Configure the tx params:
|
||||
let tx_config = DefaultExtrinsicParamsBuilder::new().tip(1234);
|
||||
|
||||
// And provide them when submitting a transaction:
|
||||
let _ = client
|
||||
.tx()
|
||||
.sign_and_submit_then_watch(&tx_payload, &dev::alice(), custom(tx_config))
|
||||
.await;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Example to utilize the `reconnecting rpc client` in subxt
|
||||
//! which hidden behind behind `--feature 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 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 chainhead backend with the reconnecting RPC client, you can do so like this:
|
||||
//
|
||||
// ```
|
||||
// use subxt::backend::chain_head:ChainHeadBackend;
|
||||
// use subxt::OnlineClient;
|
||||
//
|
||||
// let backend = ChainHeadBackend::builder().build_with_background_task(RpcClient::new(rpc.clone()));
|
||||
// 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(())
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Example to utilize the ChainHeadBackend rpc backend to subscribe to finalized blocks.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use futures::StreamExt;
|
||||
use subxt::backend::chain_head::{ChainHeadBackend, ChainHeadBackendBuilder};
|
||||
use subxt::backend::rpc::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();
|
||||
|
||||
let rpc = RpcClient::from_url("ws://localhost:9944".to_string()).await?;
|
||||
let backend: ChainHeadBackend<PolkadotConfig> =
|
||||
ChainHeadBackendBuilder::default().build_with_background_driver(rpc.clone());
|
||||
let api = OnlineClient::from_backend(std::sync::Arc::new(backend)).await?;
|
||||
|
||||
let mut blocks_sub = api.blocks().subscribe_finalized().await?.take(100);
|
||||
|
||||
while let Some(block) = blocks_sub.next().await {
|
||||
let block = block?;
|
||||
|
||||
let block_number = block.number();
|
||||
let block_hash = block.hash();
|
||||
|
||||
println!("Block #{block_number} ({block_hash})");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use pezkuwi_subxt_signer::sr25519::dev;
|
||||
|
||||
// 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 API client, configured to talk to Polkadot nodes.
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
let account = dev::alice().public_key().into();
|
||||
|
||||
// Build a storage query to access account information.
|
||||
let storage_query = polkadot::storage().system().account();
|
||||
|
||||
// Use that query to access a storage entry, fetch a result and decode the value.
|
||||
// The static address knows that fetching requires a tuple of one value, an
|
||||
// AccountId32.
|
||||
let client_at = api.storage().at_latest().await?;
|
||||
let account_info = client_at
|
||||
.entry(storage_query)?
|
||||
.fetch((account,))
|
||||
.await?
|
||||
.decode()?;
|
||||
|
||||
// The static address that we got from the subxt macro knows the expected input
|
||||
// and return types, so it is decoded into a static type for us.
|
||||
println!("Alice: {account_info:?}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::dynamic::{At, Value};
|
||||
use subxt::utils::AccountId32;
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use pezkuwi_subxt_signer::sr25519::dev;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a new API client, configured to talk to Polkadot nodes.
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Build a dynamic storage query to access account information.
|
||||
// here, we assume that there is one value to provide at this entry
|
||||
// to access a value; an AccountId32. In this example we don't know the
|
||||
// return type and so we set it to `Value`, which anything can decode into.
|
||||
let account: AccountId32 = dev::alice().public_key().into();
|
||||
let storage_query = subxt::dynamic::storage::<(AccountId32,), Value>("System", "Account");
|
||||
|
||||
// Use that query to access a storage entry, fetch a result and decode the value.
|
||||
let client_at = api.storage().at_latest().await?;
|
||||
let account_info = client_at
|
||||
.entry(storage_query)?
|
||||
.fetch((account,))
|
||||
.await?
|
||||
.decode()?;
|
||||
|
||||
// With out `Value` type we can dig in to find what we want using the `At`
|
||||
// trait and `.at()` method that this provides on the Value.
|
||||
println!(
|
||||
"Alice has free balance: {}",
|
||||
account_info.at("data").at("free").unwrap()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::ext::futures::StreamExt;
|
||||
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 API client, configured to talk to Polkadot nodes.
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Build a storage query to access account information. Same as if we were
|
||||
// fetching a single value from this entry.
|
||||
let storage_query = polkadot::storage().system().account();
|
||||
|
||||
// Use that query to access a storage entry, iterate over it and decode values.
|
||||
let client_at = api.storage().at_latest().await?;
|
||||
|
||||
// We provide an empty tuple when iterating. If the storage entry had been an N map with
|
||||
// multiple keys, then we could provide any prefix of those keys to iterate over. This is
|
||||
// statically type checked, so only a valid number/type of keys in the tuple is accepted.
|
||||
let mut values = client_at.entry(storage_query)?.iter(()).await?;
|
||||
|
||||
while let Some(kv) = values.next().await {
|
||||
let kv = kv?;
|
||||
|
||||
// The key decodes into the type that the static address knows about, in this case a
|
||||
// tuple of one entry, because the only part of the key that we can decode is the
|
||||
// AccountId32 for each user.
|
||||
let (account_id32,) = kv.key()?.decode()?;
|
||||
|
||||
// The value decodes into a statically generated type which holds account information.
|
||||
let value = kv.value().decode()?;
|
||||
|
||||
let value_data = value.data;
|
||||
println!("{account_id32}:\n {value_data:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::ext::futures::StreamExt;
|
||||
use subxt::utils::AccountId32;
|
||||
use subxt::{
|
||||
OnlineClient, PolkadotConfig,
|
||||
dynamic::{At, Value},
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a new API client, configured to talk to Polkadot nodes.
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Build a dynamic storage query to access account information.
|
||||
// here, we assume that there is one value to provide at this entry
|
||||
// to access a value; an AccountId32. In this example we don't know the
|
||||
// return type and so we set it to `Value`, which anything can decode into.
|
||||
let storage_query = subxt::dynamic::storage::<(AccountId32,), Value>("System", "Account");
|
||||
|
||||
// Use that query to access a storage entry, iterate over it and decode values.
|
||||
let client_at = api.storage().at_latest().await?;
|
||||
let mut values = client_at.entry(storage_query)?.iter(()).await?;
|
||||
|
||||
while let Some(kv) = values.next().await {
|
||||
let kv = kv?;
|
||||
|
||||
// The key decodes into the first type we provided in the address. Since there's just
|
||||
// one key, it is a tuple of one entry, an AccountId32. If we didn't know how many
|
||||
// keys or their type, we could set the key to `Vec<Value>` instead.
|
||||
let (account_id32,) = kv.key()?.decode()?;
|
||||
|
||||
// The value decodes into the second type we provided in the address. In this example,
|
||||
// we just decode it into our `Value` type and then look at the "data" field in this
|
||||
// (which implicitly assumes we get a struct shaped thing back with such a field).
|
||||
let value = kv.value().decode()?;
|
||||
|
||||
let value_data = value.at("data").unwrap();
|
||||
println!("{account_id32}:\n {value_data}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
//! This example demonstrates how to use to add a custom signer implementation to `subxt`
|
||||
//! by using the signer implementation from polkadot-sdk.
|
||||
//!
|
||||
//! Similar functionality was provided by the `substrate-compat` feature in the original `subxt` crate.
|
||||
//! which is now removed.
|
||||
|
||||
#![allow(missing_docs, unused)]
|
||||
|
||||
use sp_core::{Pair as _, sr25519};
|
||||
use subxt::config::substrate::MultiAddress;
|
||||
use subxt::{Config, OnlineClient, PolkadotConfig};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
/// A concrete PairSigner implementation which relies on `sr25519::Pair` for signing
|
||||
/// and that PolkadotConfig is the runtime configuration.
|
||||
mod pair_signer {
|
||||
use super::*;
|
||||
use sp_runtime::{
|
||||
MultiSignature as SpMultiSignature,
|
||||
traits::{IdentifyAccount, Verify},
|
||||
};
|
||||
use subxt::{
|
||||
config::substrate::{AccountId32, MultiSignature},
|
||||
tx::Signer,
|
||||
};
|
||||
|
||||
/// A [`Signer`] implementation for [`sp_core::sr25519::Pair`].
|
||||
#[derive(Clone)]
|
||||
pub struct PairSigner {
|
||||
account_id: <PolkadotConfig as Config>::AccountId,
|
||||
signer: sr25519::Pair,
|
||||
}
|
||||
|
||||
impl PairSigner {
|
||||
/// Creates a new [`Signer`] from an [`sp_core::sr25519::Pair`].
|
||||
pub fn new(signer: sr25519::Pair) -> Self {
|
||||
let account_id =
|
||||
<SpMultiSignature as Verify>::Signer::from(signer.public()).into_account();
|
||||
Self {
|
||||
// Convert `sp_core::AccountId32` to `subxt::config::substrate::AccountId32`.
|
||||
//
|
||||
// This is necessary because we use `subxt::config::substrate::AccountId32` and no
|
||||
// From/Into impls are provided between `sp_core::AccountId32` because `polkadot-sdk` isn't a direct
|
||||
// dependency in subxt.
|
||||
//
|
||||
// This can also be done by provided a wrapper type around `subxt::config::substrate::AccountId32` to implement
|
||||
// such conversions but that also most likely requires a custom `Config` with a separate `AccountId` type to work
|
||||
// properly without additional hacks.
|
||||
account_id: AccountId32(account_id.into()),
|
||||
signer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`sp_core::sr25519::Pair`] implementation used to construct this.
|
||||
pub fn signer(&self) -> &sr25519::Pair {
|
||||
&self.signer
|
||||
}
|
||||
|
||||
/// Return the account ID.
|
||||
pub fn account_id(&self) -> &AccountId32 {
|
||||
&self.account_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Signer<PolkadotConfig> for PairSigner {
|
||||
fn account_id(&self) -> <PolkadotConfig as Config>::AccountId {
|
||||
self.account_id.clone()
|
||||
}
|
||||
|
||||
fn sign(&self, signer_payload: &[u8]) -> <PolkadotConfig as Config>::Signature {
|
||||
let signature = self.signer.sign(signer_payload);
|
||||
MultiSignature::Sr25519(signature.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Create a new API client, configured to talk to Polkadot nodes.
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
let signer = {
|
||||
let acc = sr25519::Pair::from_string("//Alice", None)?;
|
||||
pair_signer::PairSigner::new(acc)
|
||||
};
|
||||
|
||||
let dest = {
|
||||
let acc = sr25519::Pair::from_string("//Bob", None)?;
|
||||
MultiAddress::Address32(acc.public().0)
|
||||
};
|
||||
|
||||
// Build a balance transfer extrinsic.
|
||||
let balance_transfer_tx = polkadot::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(dest, 100_000);
|
||||
|
||||
// Submit the balance transfer extrinsic from Alice, and wait for it to be successful
|
||||
// and in a finalized block. We get back the extrinsic events if all is well.
|
||||
let events = api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&balance_transfer_tx, &signer)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
// Find a Transfer event and print it.
|
||||
let transfer_event = events.find_first::<polkadot::balances::events::Transfer>()?;
|
||||
if let Some(event) = transfer_event {
|
||||
println!("Balance transfer success: {event:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use pezkuwi_subxt_signer::sr25519::dev;
|
||||
|
||||
// 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 API client, configured to talk to Polkadot nodes.
|
||||
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);
|
||||
|
||||
// Submit the balance transfer extrinsic from Alice, and wait for it to be successful
|
||||
// and in a finalized block. We get back the extrinsic events if all is well.
|
||||
let from = dev::alice();
|
||||
let events = api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&balance_transfer_tx, &from)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
// Find a Transfer event and print it.
|
||||
let transfer_event = events.find_first::<polkadot::balances::events::Transfer>()?;
|
||||
if let Some(event) = transfer_event {
|
||||
println!("Balance transfer success: {event:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//! Example to use subxt to talk to substrate-based nodes with ethereum accounts
|
||||
//! which is not the default for subxt which is why we need to provide a custom config.
|
||||
//!
|
||||
//! This example requires to run a local frontier/moonbeam node to work.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use subxt::OnlineClient;
|
||||
use pezkuwi_subxt_core::utils::AccountId20;
|
||||
use pezkuwi_subxt_signer::eth::{Signature, dev};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/frontier_metadata_small.scale")]
|
||||
mod eth_runtime {}
|
||||
|
||||
enum EthRuntimeConfig {}
|
||||
|
||||
impl subxt::Config for EthRuntimeConfig {
|
||||
type AccountId = AccountId20;
|
||||
type Address = AccountId20;
|
||||
type Signature = Signature;
|
||||
type Hasher = subxt::config::substrate::BlakeTwo256;
|
||||
type Header =
|
||||
subxt::config::substrate::SubstrateHeader<u32, subxt::config::substrate::BlakeTwo256>;
|
||||
type ExtrinsicParams = subxt::config::SubstrateExtrinsicParams<Self>;
|
||||
type AssetId = u32;
|
||||
}
|
||||
|
||||
#[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.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, 10_001);
|
||||
|
||||
let events = api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&balance_transfer_tx, &alith)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
let transfer_event = events.find_first::<eth_runtime::balances::events::Transfer>()?;
|
||||
if let Some(event) = transfer_event {
|
||||
println!("Balance transfer success: {event:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use pezkuwi_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::{Value, tx};
|
||||
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 pezkuwi_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 `PartialTransaction`
|
||||
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 mut partial_tx = api
|
||||
.tx()
|
||||
.create_partial(
|
||||
&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 `PartialTransaction` 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_account_and_signature(&alice.public_key().to_account_id(), &signature.into());
|
||||
|
||||
// Submit it.
|
||||
tx.submit_and_watch()
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig, tx::TxStatus};
|
||||
use pezkuwi_subxt_signer::sr25519::dev;
|
||||
|
||||
// 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 API client, configured to talk to Polkadot nodes.
|
||||
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);
|
||||
|
||||
// Submit the balance transfer extrinsic from Alice, and then monitor the
|
||||
// progress of it.
|
||||
let from = dev::alice();
|
||||
let mut balance_transfer_progress = api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&balance_transfer_tx, &from)
|
||||
.await?;
|
||||
|
||||
while let Some(status) = balance_transfer_progress.next().await {
|
||||
match status? {
|
||||
// It's finalized in a block!
|
||||
TxStatus::InFinalizedBlock(in_block) => {
|
||||
println!(
|
||||
"Transaction {:?} is finalized in block {:?}",
|
||||
in_block.extrinsic_hash(),
|
||||
in_block.block_hash()
|
||||
);
|
||||
|
||||
// grab the events and fail if no ExtrinsicSuccess event seen:
|
||||
let events = in_block.wait_for_success().await?;
|
||||
// We can look for events (this uses the static interface; we can also iterate
|
||||
// over them and dynamically decode them):
|
||||
let transfer_event = events.find_first::<polkadot::balances::events::Transfer>()?;
|
||||
|
||||
if let Some(event) = transfer_event {
|
||||
println!("Balance transfer success: {event:?}");
|
||||
} else {
|
||||
println!("Failed to find Balances::Transfer Event");
|
||||
}
|
||||
}
|
||||
// Just log any other status we encounter:
|
||||
other => {
|
||||
println!("Status: {other:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::config::polkadot::PolkadotExtrinsicParamsBuilder as Params;
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use pezkuwi_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>> {
|
||||
// Create a new API client, configured to talk to Polkadot nodes.
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Build a balance transfer extrinsic.
|
||||
let dest = dev::bob().public_key().into();
|
||||
let tx = polkadot::tx().balances().transfer_allow_death(dest, 10_000);
|
||||
|
||||
// Configure the transaction parameters; we give a small tip and set the
|
||||
// transaction to live for 32 blocks from the `latest_block` above.
|
||||
let tx_params = Params::new().tip(1_000).mortal(32).build();
|
||||
|
||||
// submit the transaction:
|
||||
let from = dev::alice();
|
||||
let hash = api.tx().sign_and_submit(&tx, &from, tx_params).await?;
|
||||
println!("Balance transfer extrinsic submitted with hash : {hash}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::error::BackendError;
|
||||
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use pezkuwi_subxt_rpcs::methods::chain_head::{ChainHeadRpcMethods, FollowEvent};
|
||||
|
||||
/// A `Stream` whose goal is to remain subscribed to `chainHead_follow`. It will re-subscribe if the subscription
|
||||
/// is ended for any reason, and it will return the current `subscription_id` as an event, along with the other
|
||||
/// follow events.
|
||||
pub struct FollowStream<Hash> {
|
||||
// Using this and not just keeping a copy of the RPC methods
|
||||
// around means that we can test this in isolation with dummy streams.
|
||||
stream_getter: FollowEventStreamGetter<Hash>,
|
||||
stream: InnerStreamState<Hash>,
|
||||
}
|
||||
|
||||
impl<Hash> std::fmt::Debug for FollowStream<Hash> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("FollowStream")
|
||||
.field("stream_getter", &"..")
|
||||
.field("stream", &self.stream)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A getter function that returns an [`FollowEventStreamFut<Hash>`].
|
||||
pub type FollowEventStreamGetter<Hash> = Box<dyn FnMut() -> FollowEventStreamFut<Hash> + Send>;
|
||||
|
||||
/// The future which will return a stream of follow events and the subscription ID for it.
|
||||
pub type FollowEventStreamFut<Hash> = Pin<
|
||||
Box<
|
||||
dyn Future<Output = Result<(FollowEventStream<Hash>, String), BackendError>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
>,
|
||||
>;
|
||||
|
||||
/// The stream of follow events.
|
||||
pub type FollowEventStream<Hash> =
|
||||
Pin<Box<dyn Stream<Item = Result<FollowEvent<Hash>, BackendError>> + Send + 'static>>;
|
||||
|
||||
/// Either a ready message with the current subscription ID, or
|
||||
/// an event from the stream itself.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FollowStreamMsg<Hash> {
|
||||
/// The stream is ready (and has a subscription ID)
|
||||
Ready(String),
|
||||
/// An event from the stream.
|
||||
Event(FollowEvent<Hash>),
|
||||
}
|
||||
|
||||
impl<Hash> FollowStreamMsg<Hash> {
|
||||
/// Return an event, or none if the message is a "ready" one.
|
||||
pub fn into_event(self) -> Option<FollowEvent<Hash>> {
|
||||
match self {
|
||||
FollowStreamMsg::Ready(_) => None,
|
||||
FollowStreamMsg::Event(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum InnerStreamState<Hash> {
|
||||
/// We've just created the stream; we'll start Initializing it
|
||||
New,
|
||||
/// We're fetching the inner subscription. Move to Ready when we have one.
|
||||
Initializing(FollowEventStreamFut<Hash>),
|
||||
/// Report back the subscription ID here, and then start ReceivingEvents.
|
||||
Ready(Option<(FollowEventStream<Hash>, String)>),
|
||||
/// We are polling for, and receiving events from the stream.
|
||||
ReceivingEvents(FollowEventStream<Hash>),
|
||||
/// We received a stop event. We'll send one on and restart the stream.
|
||||
Stopped,
|
||||
/// The stream is finished and will not restart (likely due to an error).
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl<Hash> std::fmt::Debug for InnerStreamState<Hash> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::New => write!(f, "New"),
|
||||
Self::Initializing(_) => write!(f, "Initializing(..)"),
|
||||
Self::Ready(_) => write!(f, "Ready(..)"),
|
||||
Self::ReceivingEvents(_) => write!(f, "ReceivingEvents(..)"),
|
||||
Self::Stopped => write!(f, "Stopped"),
|
||||
Self::Finished => write!(f, "Finished"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Hash> FollowStream<Hash> {
|
||||
/// Create a new [`FollowStream`] given a function which returns the stream.
|
||||
pub fn new(stream_getter: FollowEventStreamGetter<Hash>) -> Self {
|
||||
Self {
|
||||
stream_getter,
|
||||
stream: InnerStreamState::New,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`FollowStream`] given the RPC methods.
|
||||
pub fn from_methods<T: Config>(methods: ChainHeadRpcMethods<T>) -> FollowStream<HashFor<T>> {
|
||||
FollowStream {
|
||||
stream_getter: Box::new(move || {
|
||||
let methods = methods.clone();
|
||||
Box::pin(async move {
|
||||
// Make the RPC call:
|
||||
let stream = methods.chainhead_v1_follow(true).await?;
|
||||
// Extract the subscription ID:
|
||||
let Some(sub_id) = stream.subscription_id().map(ToOwned::to_owned) else {
|
||||
return Err(BackendError::Other(
|
||||
"Subscription ID expected for chainHead_follow response, but not given"
|
||||
.to_owned(),
|
||||
));
|
||||
};
|
||||
// Map stream errors into the higher level subxt one:
|
||||
let stream = stream.map_err(|e| e.into());
|
||||
let stream: FollowEventStream<HashFor<T>> = Box::pin(stream);
|
||||
// Return both:
|
||||
Ok((stream, sub_id))
|
||||
})
|
||||
}),
|
||||
stream: InnerStreamState::New,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Hash> std::marker::Unpin for FollowStream<Hash> {}
|
||||
|
||||
impl<Hash> Stream for FollowStream<Hash> {
|
||||
type Item = Result<FollowStreamMsg<Hash>, BackendError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
|
||||
loop {
|
||||
match &mut this.stream {
|
||||
InnerStreamState::New => {
|
||||
let fut = (this.stream_getter)();
|
||||
this.stream = InnerStreamState::Initializing(fut);
|
||||
continue;
|
||||
}
|
||||
InnerStreamState::Initializing(fut) => {
|
||||
match fut.poll_unpin(cx) {
|
||||
Poll::Pending => {
|
||||
return Poll::Pending;
|
||||
}
|
||||
Poll::Ready(Ok(sub_with_id)) => {
|
||||
this.stream = InnerStreamState::Ready(Some(sub_with_id));
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
InnerStreamState::Ready(stream) => {
|
||||
// We never set the Option to `None`; we just have an Option so
|
||||
// that we can take ownership of the contents easily here.
|
||||
let (sub, sub_id) = stream.take().expect("should always be Some");
|
||||
this.stream = InnerStreamState::ReceivingEvents(sub);
|
||||
return Poll::Ready(Some(Ok(FollowStreamMsg::Ready(sub_id))));
|
||||
}
|
||||
InnerStreamState::ReceivingEvents(stream) => {
|
||||
match stream.poll_next_unpin(cx) {
|
||||
Poll::Pending => {
|
||||
return Poll::Pending;
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
// No error happened but the stream ended; restart and
|
||||
// pass on a Stop message anyway.
|
||||
this.stream = InnerStreamState::Stopped;
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Some(Ok(ev))) => {
|
||||
if let FollowEvent::Stop = ev {
|
||||
// A stop event means the stream has ended, so start
|
||||
// over after passing on the stop message.
|
||||
this.stream = InnerStreamState::Stopped;
|
||||
continue;
|
||||
}
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
InnerStreamState::Stopped => {
|
||||
this.stream = InnerStreamState::New;
|
||||
return Poll::Ready(Some(Ok(FollowStreamMsg::Event(FollowEvent::Stop))));
|
||||
}
|
||||
InnerStreamState::Finished => {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) mod test_utils {
|
||||
use super::*;
|
||||
use crate::config::substrate::H256;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use pezkuwi_subxt_rpcs::methods::chain_head::{BestBlockChanged, Finalized, Initialized, NewBlock};
|
||||
|
||||
/// Given some events, returns a follow stream getter that we can use in
|
||||
/// place of the usual RPC method.
|
||||
pub fn test_stream_getter<Hash, F, I>(events: F) -> FollowEventStreamGetter<Hash>
|
||||
where
|
||||
Hash: Send + 'static,
|
||||
F: Fn() -> I + Send + 'static,
|
||||
I: IntoIterator<Item = Result<FollowEvent<Hash>, BackendError>>,
|
||||
{
|
||||
let start_idx = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
Box::new(move || {
|
||||
// Start the events from where we left off last time.
|
||||
let start_idx = start_idx.clone();
|
||||
let this_idx = start_idx.load(Ordering::Relaxed);
|
||||
let events: Vec<_> = events().into_iter().skip(this_idx).collect();
|
||||
|
||||
Box::pin(async move {
|
||||
// Increment start_idx for each event we see, so that if we get
|
||||
// the stream again, we get only the remaining events for it.
|
||||
let stream = futures::stream::iter(events).map(move |ev| {
|
||||
start_idx.fetch_add(1, Ordering::Relaxed);
|
||||
ev
|
||||
});
|
||||
|
||||
let stream: FollowEventStream<Hash> = Box::pin(stream);
|
||||
Ok((stream, format!("sub_id_{this_idx}")))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// An initialized event
|
||||
pub fn ev_initialized(n: u64) -> FollowEvent<H256> {
|
||||
FollowEvent::Initialized(Initialized {
|
||||
finalized_block_hashes: vec![H256::from_low_u64_le(n)],
|
||||
finalized_block_runtime: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A new block event
|
||||
pub fn ev_new_block(parent_n: u64, n: u64) -> FollowEvent<H256> {
|
||||
FollowEvent::NewBlock(NewBlock {
|
||||
parent_block_hash: H256::from_low_u64_le(parent_n),
|
||||
block_hash: H256::from_low_u64_le(n),
|
||||
new_runtime: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A best block event
|
||||
pub fn ev_best_block(n: u64) -> FollowEvent<H256> {
|
||||
FollowEvent::BestBlockChanged(BestBlockChanged {
|
||||
best_block_hash: H256::from_low_u64_le(n),
|
||||
})
|
||||
}
|
||||
|
||||
/// A finalized event
|
||||
pub fn ev_finalized(
|
||||
finalized_ns: impl IntoIterator<Item = u64>,
|
||||
pruned_ns: impl IntoIterator<Item = u64>,
|
||||
) -> FollowEvent<H256> {
|
||||
FollowEvent::Finalized(Finalized {
|
||||
finalized_block_hashes: finalized_ns
|
||||
.into_iter()
|
||||
.map(H256::from_low_u64_le)
|
||||
.collect(),
|
||||
pruned_block_hashes: pruned_ns.into_iter().map(H256::from_low_u64_le).collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use super::*;
|
||||
use test_utils::{ev_initialized, ev_new_block, test_stream_getter};
|
||||
|
||||
#[tokio::test]
|
||||
async fn follow_stream_provides_messages_until_error() {
|
||||
// The events we'll get back on the stream.
|
||||
let stream_getter = test_stream_getter(|| {
|
||||
[
|
||||
Ok(ev_initialized(1)),
|
||||
// Stop should lead to a drop and resubscribe:
|
||||
Ok(FollowEvent::Stop),
|
||||
Ok(FollowEvent::Stop),
|
||||
Ok(ev_new_block(1, 2)),
|
||||
// Nothing should be emitted after an error:
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Ok(ev_new_block(2, 3)),
|
||||
]
|
||||
});
|
||||
|
||||
let s = FollowStream::new(stream_getter);
|
||||
let out: Vec<_> = s.filter_map(async |e| e.ok()).collect().await;
|
||||
|
||||
// The expected response, given the above.
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![
|
||||
FollowStreamMsg::Ready("sub_id_0".to_owned()),
|
||||
FollowStreamMsg::Event(ev_initialized(1)),
|
||||
FollowStreamMsg::Event(FollowEvent::Stop),
|
||||
FollowStreamMsg::Ready("sub_id_2".to_owned()),
|
||||
FollowStreamMsg::Event(FollowEvent::Stop),
|
||||
FollowStreamMsg::Ready("sub_id_3".to_owned()),
|
||||
FollowStreamMsg::Event(ev_new_block(1, 2)),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::follow_stream_unpin::{BlockRef, FollowStreamMsg, FollowStreamUnpin};
|
||||
use crate::config::Hash;
|
||||
use crate::error::{BackendError, RpcError};
|
||||
use futures::stream::{Stream, StreamExt};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::ops::DerefMut;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use pezkuwi_subxt_rpcs::methods::chain_head::{FollowEvent, Initialized, RuntimeEvent};
|
||||
|
||||
/// A `Stream` which builds on `FollowStreamDriver`, and allows multiple subscribers to obtain events
|
||||
/// from the single underlying subscription (each being provided an `Initialized` message and all new
|
||||
/// blocks since then, as if they were each creating a unique `chainHead_follow` subscription). This
|
||||
/// is the "top" layer of our follow stream subscriptions, and the one that's interacted with elsewhere.
|
||||
#[derive(Debug)]
|
||||
pub struct FollowStreamDriver<H: Hash> {
|
||||
inner: FollowStreamUnpin<H>,
|
||||
shared: Shared<H>,
|
||||
}
|
||||
|
||||
impl<H: Hash> FollowStreamDriver<H> {
|
||||
/// Create a new [`FollowStreamDriver`]. This must be polled by some executor
|
||||
/// in order for any progress to be made. Things can subscribe to events.
|
||||
pub fn new(follow_unpin: FollowStreamUnpin<H>) -> Self {
|
||||
Self {
|
||||
inner: follow_unpin,
|
||||
shared: Shared::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a handle from which we can create new subscriptions to follow events.
|
||||
pub fn handle(&self) -> FollowStreamDriverHandle<H> {
|
||||
FollowStreamDriverHandle {
|
||||
shared: self.shared.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash> Stream for FollowStreamDriver<H> {
|
||||
type Item = Result<(), BackendError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.inner.poll_next_unpin(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(None) => {
|
||||
// Mark ourselves as done so that everything can end.
|
||||
self.shared.done();
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
|
||||
Poll::Ready(Some(Ok(item))) => {
|
||||
// Push item to any subscribers.
|
||||
self.shared.push_item(item);
|
||||
Poll::Ready(Some(Ok(())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle that can be used to create subscribers, but that doesn't
|
||||
/// itself subscribe to events.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FollowStreamDriverHandle<H: Hash> {
|
||||
shared: Shared<H>,
|
||||
}
|
||||
|
||||
impl<H: Hash> FollowStreamDriverHandle<H> {
|
||||
/// Subscribe to follow events.
|
||||
pub fn subscribe(&self) -> FollowStreamDriverSubscription<H> {
|
||||
self.shared.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
/// A subscription to events from the [`FollowStreamDriver`]. All subscriptions
|
||||
/// begin first with a `Ready` event containing the current subscription ID, and
|
||||
/// then with an `Initialized` event containing the latest finalized block and latest
|
||||
/// runtime information, and then any new/best block events and so on received since
|
||||
/// the latest finalized block.
|
||||
#[derive(Debug)]
|
||||
pub struct FollowStreamDriverSubscription<H: Hash> {
|
||||
id: usize,
|
||||
done: bool,
|
||||
shared: Shared<H>,
|
||||
local_items: VecDeque<FollowStreamMsg<BlockRef<H>>>,
|
||||
}
|
||||
|
||||
impl<H: Hash> Stream for FollowStreamDriverSubscription<H> {
|
||||
type Item = FollowStreamMsg<BlockRef<H>>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if self.done {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some(item) = self.local_items.pop_front() {
|
||||
return Poll::Ready(Some(item));
|
||||
}
|
||||
|
||||
let items = self.shared.take_items_and_save_waker(self.id, cx.waker());
|
||||
|
||||
// If no items left, mark locally as done (to avoid further locking)
|
||||
// and return None to signal done-ness.
|
||||
let Some(items) = items else {
|
||||
self.done = true;
|
||||
return Poll::Ready(None);
|
||||
};
|
||||
|
||||
// No items? We've saved the waker so we'll be told when more come.
|
||||
// Else, save the items locally and loop around to pop from them.
|
||||
if items.is_empty() {
|
||||
return Poll::Pending;
|
||||
} else {
|
||||
self.local_items = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash> FollowStreamDriverSubscription<H> {
|
||||
/// Return the current subscription ID. If the subscription has stopped, then this will
|
||||
/// wait until a new subscription has started with a new ID.
|
||||
pub async fn subscription_id(self) -> Option<String> {
|
||||
let ready_event = self
|
||||
.skip_while(|ev| std::future::ready(!matches!(ev, FollowStreamMsg::Ready(_))))
|
||||
.next()
|
||||
.await?;
|
||||
|
||||
match ready_event {
|
||||
FollowStreamMsg::Ready(sub_id) => Some(sub_id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to the follow events, ignoring any other messages.
|
||||
pub fn events(self) -> impl Stream<Item = FollowEvent<BlockRef<H>>> + Send + Sync {
|
||||
self.filter_map(|ev| std::future::ready(ev.into_event()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash> Clone for FollowStreamDriverSubscription<H> {
|
||||
fn clone(&self) -> Self {
|
||||
self.shared.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash> Drop for FollowStreamDriverSubscription<H> {
|
||||
fn drop(&mut self) {
|
||||
self.shared.remove_sub(self.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Locked shared state. The driver stream will access this state to push
|
||||
/// events to any subscribers, and subscribers will access it to pull the
|
||||
/// events destined for themselves.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Shared<H: Hash>(Arc<Mutex<SharedState<H>>>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SharedState<H: Hash> {
|
||||
done: bool,
|
||||
next_id: usize,
|
||||
subscribers: HashMap<usize, SubscriberDetails<H>>,
|
||||
/// Keep a buffer of all events that should be handed to a new subscription.
|
||||
block_events_for_new_subscriptions: VecDeque<FollowEvent<BlockRef<H>>>,
|
||||
// Keep track of the subscription ID we send out on new subs.
|
||||
current_subscription_id: Option<String>,
|
||||
// Keep track of the init message we send out on new subs.
|
||||
current_init_message: Option<Initialized<BlockRef<H>>>,
|
||||
// Runtime events by block hash; we need to track these to know
|
||||
// whether the runtime has changed when we see a finalized block notification.
|
||||
seen_runtime_events: HashMap<H, RuntimeEvent>,
|
||||
}
|
||||
|
||||
impl<H: Hash> Default for Shared<H> {
|
||||
fn default() -> Self {
|
||||
Shared(Arc::new(Mutex::new(SharedState {
|
||||
next_id: 1,
|
||||
done: false,
|
||||
subscribers: HashMap::new(),
|
||||
current_init_message: None,
|
||||
current_subscription_id: None,
|
||||
seen_runtime_events: HashMap::new(),
|
||||
block_events_for_new_subscriptions: VecDeque::new(),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash> Shared<H> {
|
||||
/// Set the shared state to "done"; no more items will be handed to it.
|
||||
pub fn done(&self) {
|
||||
let mut shared = self.0.lock().unwrap();
|
||||
shared.done = true;
|
||||
|
||||
// Wake up all subscribers so they get notified that the backend was closed
|
||||
for details in shared.subscribers.values_mut() {
|
||||
if let Some(waker) = details.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleanup a subscription.
|
||||
pub fn remove_sub(&self, sub_id: usize) {
|
||||
let mut shared = self.0.lock().unwrap();
|
||||
shared.subscribers.remove(&sub_id);
|
||||
}
|
||||
|
||||
/// Take items for some subscription ID and save the waker.
|
||||
pub fn take_items_and_save_waker(
|
||||
&self,
|
||||
sub_id: usize,
|
||||
waker: &Waker,
|
||||
) -> Option<VecDeque<FollowStreamMsg<BlockRef<H>>>> {
|
||||
let mut shared = self.0.lock().unwrap();
|
||||
|
||||
let is_done = shared.done;
|
||||
let details = shared.subscribers.get_mut(&sub_id)?;
|
||||
|
||||
// no more items to pull, and stream closed, so return None.
|
||||
if details.items.is_empty() && is_done {
|
||||
return None;
|
||||
}
|
||||
|
||||
// else, take whatever items, and save the waker if not done yet.
|
||||
let items = std::mem::take(&mut details.items);
|
||||
if !is_done {
|
||||
details.waker = Some(waker.clone());
|
||||
}
|
||||
Some(items)
|
||||
}
|
||||
|
||||
/// Push a new item out to subscribers.
|
||||
pub fn push_item(&self, item: FollowStreamMsg<BlockRef<H>>) {
|
||||
let mut shared = self.0.lock().unwrap();
|
||||
let shared = shared.deref_mut();
|
||||
|
||||
// broadcast item to subscribers:
|
||||
for details in shared.subscribers.values_mut() {
|
||||
details.items.push_back(item.clone());
|
||||
if let Some(waker) = details.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep our buffer of ready/block events up-to-date:
|
||||
match item {
|
||||
FollowStreamMsg::Ready(sub_id) => {
|
||||
// Set new subscription ID when it comes in.
|
||||
shared.current_subscription_id = Some(sub_id);
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::Initialized(ev)) => {
|
||||
// New subscriptions will be given this init message:
|
||||
shared.current_init_message = Some(ev.clone());
|
||||
// Clear block cache (since a new finalized block hash is seen):
|
||||
shared.block_events_for_new_subscriptions.clear();
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::Finalized(finalized_ev)) => {
|
||||
// Update the init message that we'll hand out to new subscriptions. If the init message
|
||||
// is `None` for some reason, we just ignore this step.
|
||||
if let Some(init_message) = &mut shared.current_init_message {
|
||||
// Find the latest runtime update that's been finalized.
|
||||
let newest_runtime = finalized_ev
|
||||
.finalized_block_hashes
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|h| shared.seen_runtime_events.get(&h.hash()).cloned())
|
||||
.next();
|
||||
|
||||
shared.seen_runtime_events.clear();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// The last finalized block will be reported as Initialized by our driver,
|
||||
// therefore there is no need to report NewBlock and BestBlock events for it.
|
||||
// If the Finalized event reported multiple finalized hashes, we only care about
|
||||
// the state at the head of the chain, therefore it is correct to remove those as well.
|
||||
// Idem for the pruned hashes; they will never be reported again and we remove
|
||||
// them from the window of events.
|
||||
let to_remove: HashSet<H> = finalized_ev
|
||||
.finalized_block_hashes
|
||||
.iter()
|
||||
.chain(finalized_ev.pruned_block_hashes.iter())
|
||||
.map(|h| h.hash())
|
||||
.collect();
|
||||
|
||||
shared
|
||||
.block_events_for_new_subscriptions
|
||||
.retain(|ev| match ev {
|
||||
FollowEvent::NewBlock(new_block_ev) => {
|
||||
!to_remove.contains(&new_block_ev.block_hash.hash())
|
||||
}
|
||||
FollowEvent::BestBlockChanged(best_block_ev) => {
|
||||
!to_remove.contains(&best_block_ev.best_block_hash.hash())
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::NewBlock(new_block_ev)) => {
|
||||
// If a new runtime is seen, note it so that when a block is finalized, we
|
||||
// can associate that with a runtime update having happened.
|
||||
if let Some(runtime_event) = &new_block_ev.new_runtime {
|
||||
shared
|
||||
.seen_runtime_events
|
||||
.insert(new_block_ev.block_hash.hash(), runtime_event.clone());
|
||||
}
|
||||
|
||||
shared
|
||||
.block_events_for_new_subscriptions
|
||||
.push_back(FollowEvent::NewBlock(new_block_ev));
|
||||
}
|
||||
FollowStreamMsg::Event(ev @ FollowEvent::BestBlockChanged(_)) => {
|
||||
shared.block_events_for_new_subscriptions.push_back(ev);
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::Stop) => {
|
||||
// On a stop event, clear everything. Wait for resubscription and new ready/initialised events.
|
||||
shared.block_events_for_new_subscriptions.clear();
|
||||
shared.current_subscription_id = None;
|
||||
shared.current_init_message = None;
|
||||
}
|
||||
_ => {
|
||||
// We don't buffer any other events.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new subscription.
|
||||
pub fn subscribe(&self) -> FollowStreamDriverSubscription<H> {
|
||||
let mut shared = self.0.lock().unwrap();
|
||||
|
||||
let id = shared.next_id;
|
||||
shared.next_id += 1;
|
||||
|
||||
shared.subscribers.insert(
|
||||
id,
|
||||
SubscriberDetails {
|
||||
items: VecDeque::new(),
|
||||
waker: None,
|
||||
},
|
||||
);
|
||||
|
||||
// Any new subscription should start with a "Ready" message and then an "Initialized"
|
||||
// message, and then any non-finalized block events since that. If these don't exist,
|
||||
// it means the subscription is currently stopped, and we should expect new Ready/Init
|
||||
// messages anyway once it restarts.
|
||||
let mut local_items = VecDeque::new();
|
||||
if let Some(sub_id) = &shared.current_subscription_id {
|
||||
local_items.push_back(FollowStreamMsg::Ready(sub_id.clone()));
|
||||
}
|
||||
if let Some(init_msg) = &shared.current_init_message {
|
||||
local_items.push_back(FollowStreamMsg::Event(FollowEvent::Initialized(
|
||||
init_msg.clone(),
|
||||
)));
|
||||
}
|
||||
for ev in &shared.block_events_for_new_subscriptions {
|
||||
local_items.push_back(FollowStreamMsg::Event(ev.clone()));
|
||||
}
|
||||
|
||||
drop(shared);
|
||||
|
||||
FollowStreamDriverSubscription {
|
||||
id,
|
||||
done: false,
|
||||
shared: self.clone(),
|
||||
local_items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Details for a given subscriber: any items it's not yet claimed,
|
||||
/// and a way to wake it up when there are more items for it.
|
||||
#[derive(Debug)]
|
||||
struct SubscriberDetails<H: Hash> {
|
||||
items: VecDeque<FollowStreamMsg<BlockRef<H>>>,
|
||||
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<H: Hash, F> {
|
||||
stream: FollowStreamDriverSubscription<H>,
|
||||
sub_id: Option<String>,
|
||||
last_seen_block: Option<BlockRef<H>>,
|
||||
f: F,
|
||||
is_done: bool,
|
||||
}
|
||||
|
||||
impl<H: Hash, F> Unpin for FollowStreamFinalizedHeads<H, F> {}
|
||||
|
||||
impl<H, F> FollowStreamFinalizedHeads<H, F>
|
||||
where
|
||||
H: Hash,
|
||||
F: Fn(FollowEvent<BlockRef<H>>) -> Vec<BlockRef<H>>,
|
||||
{
|
||||
pub fn new(stream: FollowStreamDriverSubscription<H>, f: F) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
sub_id: None,
|
||||
last_seen_block: None,
|
||||
f,
|
||||
is_done: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, F> Stream for FollowStreamFinalizedHeads<H, F>
|
||||
where
|
||||
H: Hash,
|
||||
F: Fn(FollowEvent<BlockRef<H>>) -> Vec<BlockRef<H>>,
|
||||
{
|
||||
type Item = Result<(String, Vec<BlockRef<H>>), BackendError>;
|
||||
|
||||
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::ClientError(
|
||||
pezkuwi_subxt_rpcs::Error::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;
|
||||
use super::*;
|
||||
|
||||
/// Return a `FollowStreamDriver`
|
||||
pub fn test_follow_stream_driver_getter<H, F, I>(
|
||||
events: F,
|
||||
max_life: usize,
|
||||
) -> FollowStreamDriver<H>
|
||||
where
|
||||
H: Hash + 'static,
|
||||
F: Fn() -> I + Send + 'static,
|
||||
I: IntoIterator<Item = Result<FollowEvent<H>, BackendError>>,
|
||||
{
|
||||
let (stream, _) = test_unpin_stream_getter(events, max_life);
|
||||
FollowStreamDriver::new(stream)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use futures::TryStreamExt;
|
||||
use primitive_types::H256;
|
||||
|
||||
use super::super::follow_stream::test_utils::{
|
||||
ev_best_block, ev_finalized, ev_initialized, ev_new_block,
|
||||
};
|
||||
use super::super::follow_stream_unpin::test_utils::{
|
||||
ev_best_block_ref, ev_finalized_ref, ev_initialized_ref, ev_new_block_ref,
|
||||
};
|
||||
use super::test_utils::test_follow_stream_driver_getter;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn follow_stream_driver_is_sendable() {
|
||||
fn assert_send<T: Send + 'static>(_: T) {}
|
||||
let stream_getter = test_follow_stream_driver_getter(|| [Ok(ev_initialized(1))], 10);
|
||||
assert_send(stream_getter);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribers_all_receive_events_and_finish_gracefully_on_error() {
|
||||
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], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
10,
|
||||
);
|
||||
|
||||
let handle = driver.handle();
|
||||
|
||||
let a = handle.subscribe();
|
||||
let b = handle.subscribe();
|
||||
let c = handle.subscribe();
|
||||
|
||||
// Drive to completion (the sort of real life usage I'd expect):
|
||||
tokio::spawn(async move { while driver.next().await.is_some() {} });
|
||||
|
||||
let a_vec: Vec<_> = a.collect().await;
|
||||
let b_vec: Vec<_> = b.collect().await;
|
||||
let c_vec: Vec<_> = c.collect().await;
|
||||
|
||||
let expected = vec![
|
||||
FollowStreamMsg::Ready("sub_id_0".into()),
|
||||
FollowStreamMsg::Event(ev_initialized_ref(0)),
|
||||
FollowStreamMsg::Event(ev_new_block_ref(0, 1)),
|
||||
FollowStreamMsg::Event(ev_best_block_ref(1)),
|
||||
FollowStreamMsg::Event(ev_finalized_ref([1])),
|
||||
];
|
||||
|
||||
assert_eq!(a_vec, expected);
|
||||
assert_eq!(b_vec, expected);
|
||||
assert_eq!(c_vec, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribers_receive_block_events_from_last_finalised() {
|
||||
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(ev_new_block(1, 2)),
|
||||
Ok(ev_new_block(2, 3)),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
10,
|
||||
);
|
||||
|
||||
// Skip past ready, init, new, best events.
|
||||
let _r = driver.next().await.unwrap();
|
||||
let _i0 = driver.next().await.unwrap();
|
||||
let _n1 = driver.next().await.unwrap();
|
||||
let _b1 = driver.next().await.unwrap();
|
||||
|
||||
// THEN subscribe; subscription should still receive them:
|
||||
let evs: Vec<_> = driver.handle().subscribe().take(4).collect().await;
|
||||
let expected = vec![
|
||||
FollowStreamMsg::Ready("sub_id_0".into()),
|
||||
FollowStreamMsg::Event(ev_initialized_ref(0)),
|
||||
FollowStreamMsg::Event(ev_new_block_ref(0, 1)),
|
||||
FollowStreamMsg::Event(ev_best_block_ref(1)),
|
||||
];
|
||||
assert_eq!(evs, expected);
|
||||
|
||||
// Skip past finalized 1, new 2, new 3 events
|
||||
let _f1 = driver.next().await.unwrap();
|
||||
let _n2 = driver.next().await.unwrap();
|
||||
let _n3 = driver.next().await.unwrap();
|
||||
|
||||
// THEN subscribe again; new subs will see an updated initialized message
|
||||
// with the latest finalized block hash.
|
||||
let evs: Vec<_> = driver.handle().subscribe().take(4).collect().await;
|
||||
let expected = vec![
|
||||
FollowStreamMsg::Ready("sub_id_0".into()),
|
||||
FollowStreamMsg::Event(ev_initialized_ref(1)),
|
||||
FollowStreamMsg::Event(ev_new_block_ref(1, 2)),
|
||||
FollowStreamMsg::Event(ev_new_block_ref(2, 3)),
|
||||
];
|
||||
assert_eq!(evs, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribers_receive_new_blocks_before_subscribing() {
|
||||
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_new_block(1, 2)),
|
||||
Ok(ev_new_block(2, 3)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
10,
|
||||
);
|
||||
|
||||
// Skip to the first finalized block F1.
|
||||
let _r = driver.next().await.unwrap();
|
||||
let _i0 = driver.next().await.unwrap();
|
||||
let _n1 = driver.next().await.unwrap();
|
||||
let _b1 = driver.next().await.unwrap();
|
||||
let _n2 = driver.next().await.unwrap();
|
||||
let _n3 = driver.next().await.unwrap();
|
||||
let _f1 = driver.next().await.unwrap();
|
||||
|
||||
// THEN subscribe; and make sure new block 1 and 2 are received.
|
||||
let evs: Vec<_> = driver.handle().subscribe().take(4).collect().await;
|
||||
let expected = vec![
|
||||
FollowStreamMsg::Ready("sub_id_0".into()),
|
||||
FollowStreamMsg::Event(ev_initialized_ref(1)),
|
||||
FollowStreamMsg::Event(ev_new_block_ref(1, 2)),
|
||||
FollowStreamMsg::Event(ev_new_block_ref(2, 3)),
|
||||
];
|
||||
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(BackendError::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(BackendError::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(BackendError::Rpc(RpcError::ClientError(pezkuwi_subxt_rpcs::Error::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))]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,813 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::ChainHeadRpcMethods;
|
||||
use super::follow_stream::FollowStream;
|
||||
use crate::config::{Config, Hash, HashFor};
|
||||
use crate::error::BackendError;
|
||||
use futures::stream::{FuturesUnordered, Stream, StreamExt};
|
||||
use pezkuwi_subxt_rpcs::methods::chain_head::{
|
||||
BestBlockChanged, Finalized, FollowEvent, Initialized, NewBlock,
|
||||
};
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
|
||||
/// The type of stream item.
|
||||
pub use super::follow_stream::FollowStreamMsg;
|
||||
|
||||
/// A `Stream` which builds on `FollowStream`, and handles pinning. It replaces any block hash seen in
|
||||
/// the follow events with a `BlockRef` which, when all clones are dropped, will lead to an "unpin" call
|
||||
/// for that block hash being queued. It will also automatically unpin any blocks that exceed a given max
|
||||
/// age, to try and prevent the underlying stream from ending (and _all_ blocks from being unpinned as a
|
||||
/// result). Put simply, it tries to keep every block pinned as long as possible until the block is no longer
|
||||
/// used anywhere.
|
||||
#[derive(Debug)]
|
||||
pub struct FollowStreamUnpin<H: Hash> {
|
||||
// The underlying stream of events.
|
||||
inner: FollowStream<H>,
|
||||
// A method to call to unpin a block, given a block hash and a subscription ID.
|
||||
unpin_method: UnpinMethodHolder<H>,
|
||||
// Futures for sending unpin events that we'll poll to completion as
|
||||
// part of polling the stream as a whole.
|
||||
unpin_futs: FuturesUnordered<UnpinFut>,
|
||||
// Each time a new finalized block is seen, we give it an age of `next_rel_block_age`,
|
||||
// and then increment this ready for the next finalized block. So, the first finalized
|
||||
// block will have an age of 0, the next 1, 2, 3 and so on. We can then use `max_block_life`
|
||||
// to say "unpin all blocks with an age < (next_rel_block_age-1) - max_block_life".
|
||||
next_rel_block_age: usize,
|
||||
// The latest ID of the FollowStream subscription, which we can use
|
||||
// to unpin blocks.
|
||||
subscription_id: Option<Arc<str>>,
|
||||
// The longest period a block can be pinned for.
|
||||
max_block_life: usize,
|
||||
// The currently seen and pinned blocks.
|
||||
pinned: HashMap<H, PinnedDetails<H>>,
|
||||
// Shared state about blocks we've flagged to unpin from elsewhere
|
||||
unpin_flags: UnpinFlags<H>,
|
||||
}
|
||||
|
||||
// Just a wrapper to make implementing debug on the whole thing easier.
|
||||
struct UnpinMethodHolder<H>(UnpinMethod<H>);
|
||||
impl<H> std::fmt::Debug for UnpinMethodHolder<H> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"UnpinMethodHolder(Box<dyn FnMut(Hash, Arc<str>) -> UnpinFut>)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of the unpin method that we need to provide.
|
||||
pub type UnpinMethod<H> = Box<dyn FnMut(H, Arc<str>) -> UnpinFut + Send>;
|
||||
|
||||
/// The future returned from [`UnpinMethod`].
|
||||
pub type UnpinFut = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
|
||||
|
||||
impl<H: Hash> std::marker::Unpin for FollowStreamUnpin<H> {}
|
||||
|
||||
impl<H: Hash> Stream for FollowStreamUnpin<H> {
|
||||
type Item = Result<FollowStreamMsg<BlockRef<H>>, BackendError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut();
|
||||
|
||||
loop {
|
||||
// Poll any queued unpin tasks.
|
||||
let unpin_futs_are_pending = match this.unpin_futs.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(())) => continue,
|
||||
Poll::Ready(None) => false,
|
||||
Poll::Pending => true,
|
||||
};
|
||||
|
||||
// Poll the inner stream for the next event.
|
||||
let Poll::Ready(ev) = this.inner.poll_next_unpin(cx) else {
|
||||
return Poll::Pending;
|
||||
};
|
||||
|
||||
let Some(ev) = ev else {
|
||||
// if the stream is done, but `unpin_futs` are still pending, then
|
||||
// return pending here so that they are still driven to completion.
|
||||
// Else, return `Ready(None)` to signal nothing left to do.
|
||||
return match unpin_futs_are_pending {
|
||||
true => Poll::Pending,
|
||||
false => Poll::Ready(None),
|
||||
};
|
||||
};
|
||||
|
||||
// Error? just return it and do nothing further.
|
||||
let ev = match ev {
|
||||
Ok(ev) => ev,
|
||||
Err(e) => {
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
}
|
||||
};
|
||||
|
||||
// React to any actual FollowEvent we get back.
|
||||
let ev = match ev {
|
||||
FollowStreamMsg::Ready(subscription_id) => {
|
||||
// update the subscription ID we'll use to unpin things.
|
||||
this.subscription_id = Some(subscription_id.clone().into());
|
||||
|
||||
FollowStreamMsg::Ready(subscription_id)
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::Initialized(details)) => {
|
||||
let mut finalized_block_hashes =
|
||||
Vec::with_capacity(details.finalized_block_hashes.len());
|
||||
|
||||
// Pin each of the finalized blocks. None of them will show up again (except as a
|
||||
// parent block), and so they can all be unpinned immediately at any time. Increment
|
||||
// the block age for each one, so that older finalized blocks are pruned first.
|
||||
for finalized_block in &details.finalized_block_hashes {
|
||||
let rel_block_age = this.next_rel_block_age;
|
||||
let block_ref =
|
||||
this.pin_unpinnable_block_at(rel_block_age, *finalized_block);
|
||||
|
||||
finalized_block_hashes.push(block_ref);
|
||||
this.next_rel_block_age += 1;
|
||||
}
|
||||
|
||||
FollowStreamMsg::Event(FollowEvent::Initialized(Initialized {
|
||||
finalized_block_hashes,
|
||||
finalized_block_runtime: details.finalized_block_runtime,
|
||||
}))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::NewBlock(details)) => {
|
||||
// One bigger than our parent, and if no parent seen (maybe it was
|
||||
// unpinned already), then one bigger than the last finalized block num
|
||||
// as a best guess.
|
||||
let parent_rel_block_age = this
|
||||
.pinned
|
||||
.get(&details.parent_block_hash)
|
||||
.map(|p| p.rel_block_age)
|
||||
.unwrap_or(this.next_rel_block_age.saturating_sub(1));
|
||||
|
||||
let block_ref = this.pin_block_at(parent_rel_block_age + 1, details.block_hash);
|
||||
let parent_block_ref =
|
||||
this.pin_block_at(parent_rel_block_age, details.parent_block_hash);
|
||||
|
||||
FollowStreamMsg::Event(FollowEvent::NewBlock(NewBlock {
|
||||
block_hash: block_ref,
|
||||
parent_block_hash: parent_block_ref,
|
||||
new_runtime: details.new_runtime,
|
||||
}))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::BestBlockChanged(details)) => {
|
||||
// We expect this block to already exist, so it'll keep its existing block_num,
|
||||
// but worst case it'll just get the current finalized block_num + 1.
|
||||
let rel_block_age = this.next_rel_block_age;
|
||||
let block_ref = this.pin_block_at(rel_block_age, details.best_block_hash);
|
||||
|
||||
FollowStreamMsg::Event(FollowEvent::BestBlockChanged(BestBlockChanged {
|
||||
best_block_hash: block_ref,
|
||||
}))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::Finalized(details)) => {
|
||||
let finalized_block_refs: Vec<_> = details
|
||||
.finalized_block_hashes
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, hash)| {
|
||||
// These blocks _should_ exist already and so will have a known block num,
|
||||
// but if they don't, we just increment the num from the last finalized block
|
||||
// we saw, which should be accurate.
|
||||
//
|
||||
// `pin_unpinnable_block_at` indicates that the block will not show up in future events
|
||||
// (They will show up as a parent block, but we don't care about that right now).
|
||||
let rel_block_age = this.next_rel_block_age + idx;
|
||||
this.pin_unpinnable_block_at(rel_block_age, hash)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Our relative block height is increased by however many finalized
|
||||
// blocks we've seen.
|
||||
this.next_rel_block_age += finalized_block_refs.len();
|
||||
|
||||
let pruned_block_refs: Vec<_> = details
|
||||
.pruned_block_hashes
|
||||
.into_iter()
|
||||
.map(|hash| {
|
||||
// We should know about these, too, and if not we set their age to last_finalized + 1.
|
||||
//
|
||||
// `pin_unpinnable_block_at` indicates that the block will not show up in future events.
|
||||
let rel_block_age = this.next_rel_block_age;
|
||||
this.pin_unpinnable_block_at(rel_block_age, hash)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// At this point, we also check to see which blocks we should submit unpin events
|
||||
// for. We will unpin:
|
||||
// - Any block that's older than the max age.
|
||||
// - Any block that has no references left (ie has been dropped) that _also_ has
|
||||
// showed up in the pruned list in a finalized event (so it will never be in another event).
|
||||
this.unpin_blocks(cx.waker());
|
||||
|
||||
FollowStreamMsg::Event(FollowEvent::Finalized(Finalized {
|
||||
finalized_block_hashes: finalized_block_refs,
|
||||
pruned_block_hashes: pruned_block_refs,
|
||||
}))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::Stop) => {
|
||||
// clear out "old" things that are no longer applicable since
|
||||
// the subscription has ended (a new one will be created under the hood, at
|
||||
// which point we'll get given a new subscription ID.
|
||||
this.subscription_id = None;
|
||||
this.pinned.clear();
|
||||
this.unpin_futs.clear();
|
||||
this.unpin_flags.lock().unwrap().clear();
|
||||
this.next_rel_block_age = 0;
|
||||
|
||||
FollowStreamMsg::Event(FollowEvent::Stop)
|
||||
}
|
||||
// These events aren't interesting; we just forward them on:
|
||||
FollowStreamMsg::Event(FollowEvent::OperationBodyDone(details)) => {
|
||||
FollowStreamMsg::Event(FollowEvent::OperationBodyDone(details))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::OperationCallDone(details)) => {
|
||||
FollowStreamMsg::Event(FollowEvent::OperationCallDone(details))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::OperationStorageItems(details)) => {
|
||||
FollowStreamMsg::Event(FollowEvent::OperationStorageItems(details))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::OperationWaitingForContinue(details)) => {
|
||||
FollowStreamMsg::Event(FollowEvent::OperationWaitingForContinue(details))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::OperationStorageDone(details)) => {
|
||||
FollowStreamMsg::Event(FollowEvent::OperationStorageDone(details))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::OperationInaccessible(details)) => {
|
||||
FollowStreamMsg::Event(FollowEvent::OperationInaccessible(details))
|
||||
}
|
||||
FollowStreamMsg::Event(FollowEvent::OperationError(details)) => {
|
||||
FollowStreamMsg::Event(FollowEvent::OperationError(details))
|
||||
}
|
||||
};
|
||||
|
||||
// Return our event.
|
||||
return Poll::Ready(Some(Ok(ev)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash> FollowStreamUnpin<H> {
|
||||
/// Create a new [`FollowStreamUnpin`].
|
||||
pub fn new(
|
||||
follow_stream: FollowStream<H>,
|
||||
unpin_method: UnpinMethod<H>,
|
||||
max_block_life: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: follow_stream,
|
||||
unpin_method: UnpinMethodHolder(unpin_method),
|
||||
max_block_life,
|
||||
pinned: Default::default(),
|
||||
subscription_id: None,
|
||||
next_rel_block_age: 0,
|
||||
unpin_flags: Default::default(),
|
||||
unpin_futs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`FollowStreamUnpin`] given the RPC methods.
|
||||
pub fn from_methods<T: Config>(
|
||||
follow_stream: FollowStream<HashFor<T>>,
|
||||
methods: ChainHeadRpcMethods<T>,
|
||||
max_block_life: usize,
|
||||
) -> FollowStreamUnpin<HashFor<T>> {
|
||||
let unpin_method = Box::new(move |hash: HashFor<T>, sub_id: Arc<str>| {
|
||||
let methods = methods.clone();
|
||||
let fut: UnpinFut = Box::pin(async move {
|
||||
// We ignore any errors trying to unpin at the moment.
|
||||
let _ = methods.chainhead_v1_unpin(&sub_id, hash).await;
|
||||
});
|
||||
fut
|
||||
});
|
||||
|
||||
FollowStreamUnpin::new(follow_stream, unpin_method, max_block_life)
|
||||
}
|
||||
|
||||
/// Is the block hash currently pinned.
|
||||
pub fn is_pinned(&self, hash: &H) -> bool {
|
||||
self.pinned.contains_key(hash)
|
||||
}
|
||||
|
||||
/// Pin a block, or return the reference to an already-pinned block. If the block has been registered to
|
||||
/// be unpinned, we'll clear those flags, so that it won't be unpinned. If the unpin request has already
|
||||
/// been sent though, then the block will be unpinned.
|
||||
fn pin_block_at(&mut self, rel_block_age: usize, hash: H) -> BlockRef<H> {
|
||||
self.pin_block_at_setting_unpinnable_flag(rel_block_age, hash, false)
|
||||
}
|
||||
|
||||
/// Pin a block, or return the reference to an already-pinned block.
|
||||
///
|
||||
/// This is the same as [`Self::pin_block_at`], except that it also marks the block as being unpinnable now,
|
||||
/// which should be done for any block that will no longer be seen in future events.
|
||||
fn pin_unpinnable_block_at(&mut self, rel_block_age: usize, hash: H) -> BlockRef<H> {
|
||||
self.pin_block_at_setting_unpinnable_flag(rel_block_age, hash, true)
|
||||
}
|
||||
|
||||
fn pin_block_at_setting_unpinnable_flag(
|
||||
&mut self,
|
||||
rel_block_age: usize,
|
||||
hash: H,
|
||||
can_be_unpinned: bool,
|
||||
) -> BlockRef<H> {
|
||||
let entry = self
|
||||
.pinned
|
||||
.entry(hash)
|
||||
// If there's already an entry, then clear any unpin_flags and update the
|
||||
// can_be_unpinned status (this can become true but cannot become false again
|
||||
// once true).
|
||||
.and_modify(|entry| {
|
||||
entry.can_be_unpinned = entry.can_be_unpinned || can_be_unpinned;
|
||||
self.unpin_flags.lock().unwrap().remove(&hash);
|
||||
})
|
||||
// If there's not an entry already, make one and return it.
|
||||
.or_insert_with(|| PinnedDetails {
|
||||
rel_block_age,
|
||||
block_ref: BlockRef {
|
||||
inner: Arc::new(BlockRefInner {
|
||||
hash,
|
||||
unpin_flags: self.unpin_flags.clone(),
|
||||
}),
|
||||
},
|
||||
can_be_unpinned,
|
||||
});
|
||||
|
||||
entry.block_ref.clone()
|
||||
}
|
||||
|
||||
/// Unpin any blocks that are either too old, or have the unpin flag set and are old enough.
|
||||
fn unpin_blocks(&mut self, waker: &Waker) {
|
||||
let mut unpin_flags = self.unpin_flags.lock().unwrap();
|
||||
|
||||
// This gets the age of the last finalized block.
|
||||
let rel_block_age = self.next_rel_block_age.saturating_sub(1);
|
||||
|
||||
// If we asked to unpin and there was no subscription_id, then there's nothing we can do,
|
||||
// and nothing will need unpinning now anyway.
|
||||
let Some(sub_id) = &self.subscription_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut blocks_to_unpin = vec![];
|
||||
for (hash, details) in &self.pinned {
|
||||
if rel_block_age.saturating_sub(details.rel_block_age) >= self.max_block_life
|
||||
|| (unpin_flags.contains(hash) && details.can_be_unpinned)
|
||||
{
|
||||
// The block is too old, or it's been flagged to be unpinned and won't be in a future
|
||||
// backend event, so we can unpin it for real now.
|
||||
blocks_to_unpin.push(*hash);
|
||||
// Clear it from our unpin flags if present so that we don't try to unpin it again.
|
||||
unpin_flags.remove(hash);
|
||||
}
|
||||
}
|
||||
|
||||
// Release our lock on unpin_flags ASAP.
|
||||
drop(unpin_flags);
|
||||
|
||||
// No need to call the waker etc if nothing to do:
|
||||
if blocks_to_unpin.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for hash in blocks_to_unpin {
|
||||
self.pinned.remove(&hash);
|
||||
let fut = (self.unpin_method.0)(hash, sub_id.clone());
|
||||
self.unpin_futs.push(fut);
|
||||
}
|
||||
|
||||
// Any new futures pushed above need polling to start. We could
|
||||
// just wait for the next stream event, but let's wake the task to
|
||||
// have it polled sooner, just in case it's slow to receive things.
|
||||
waker.wake_by_ref();
|
||||
}
|
||||
}
|
||||
|
||||
// The set of block hashes that can be unpinned when ready.
|
||||
// BlockRefs write to this when they are dropped.
|
||||
type UnpinFlags<H> = Arc<Mutex<HashSet<H>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PinnedDetails<H: Hash> {
|
||||
/// Relatively speaking, how old is the block? When we start following
|
||||
/// blocks, the first finalized block gets an age of 0, the second an age
|
||||
/// of 1 and so on.
|
||||
rel_block_age: usize,
|
||||
/// A block ref we can hand out to keep blocks pinned.
|
||||
/// Because we store one here until it's unpinned, the live count
|
||||
/// will only drop to 1 when no external refs are left.
|
||||
block_ref: BlockRef<H>,
|
||||
/// Has this block showed up in the list of pruned blocks, or has it
|
||||
/// been finalized? In this case, it can now been pinned as it won't
|
||||
/// show up again in future events (except as a "parent block" of some
|
||||
/// new block, which we're currently ignoring).
|
||||
can_be_unpinned: bool,
|
||||
}
|
||||
|
||||
/// All blocks reported will be wrapped in this.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockRef<H: Hash> {
|
||||
inner: Arc<BlockRefInner<H>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BlockRefInner<H> {
|
||||
hash: H,
|
||||
unpin_flags: UnpinFlags<H>,
|
||||
}
|
||||
|
||||
impl<H: Hash> BlockRef<H> {
|
||||
/// For testing purposes only, create a BlockRef from a hash
|
||||
/// that isn't pinned.
|
||||
#[cfg(test)]
|
||||
pub fn new(hash: H) -> Self {
|
||||
BlockRef {
|
||||
inner: Arc::new(BlockRefInner {
|
||||
hash,
|
||||
unpin_flags: Default::default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the hash for this block.
|
||||
pub fn hash(&self) -> H {
|
||||
self.inner.hash
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash> PartialEq for BlockRef<H> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner.hash == other.inner.hash
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash> PartialEq<H> for BlockRef<H> {
|
||||
fn eq(&self, other: &H) -> bool {
|
||||
&self.inner.hash == other
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash> Drop for BlockRef<H> {
|
||||
fn drop(&mut self) {
|
||||
// PinnedDetails keeps one ref, so if this is the second ref, it's the
|
||||
// only "external" one left and we should ask to unpin it now. if it's
|
||||
// the only ref remaining, it means that it's already been unpinned, so
|
||||
// nothing to do here anyway.
|
||||
if Arc::strong_count(&self.inner) == 2 {
|
||||
if let Ok(mut unpin_flags) = self.inner.unpin_flags.lock() {
|
||||
unpin_flags.insert(self.inner.hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) mod test_utils {
|
||||
use super::super::follow_stream::{FollowStream, test_utils::test_stream_getter};
|
||||
use super::*;
|
||||
use crate::config::substrate::H256;
|
||||
|
||||
pub type UnpinRx<H> = std::sync::mpsc::Receiver<(H, Arc<str>)>;
|
||||
|
||||
/// Get a [`FollowStreamUnpin`] from an iterator over events.
|
||||
pub fn test_unpin_stream_getter<H, F, I>(
|
||||
events: F,
|
||||
max_life: usize,
|
||||
) -> (FollowStreamUnpin<H>, UnpinRx<H>)
|
||||
where
|
||||
H: Hash + 'static,
|
||||
F: Fn() -> I + Send + 'static,
|
||||
I: IntoIterator<Item = Result<FollowEvent<H>, BackendError>>,
|
||||
{
|
||||
// Unpin requests will come here so that we can look out for them.
|
||||
let (unpin_tx, unpin_rx) = std::sync::mpsc::channel();
|
||||
|
||||
let follow_stream = FollowStream::new(test_stream_getter(events));
|
||||
let unpin_method: UnpinMethod<H> = Box::new(move |hash, sub_id| {
|
||||
unpin_tx.send((hash, sub_id)).unwrap();
|
||||
Box::pin(std::future::ready(()))
|
||||
});
|
||||
|
||||
let follow_unpin = FollowStreamUnpin::new(follow_stream, unpin_method, max_life);
|
||||
(follow_unpin, unpin_rx)
|
||||
}
|
||||
|
||||
/// Assert that the unpinned blocks sent from the `UnpinRx` channel match the items given.
|
||||
pub fn assert_from_unpin_rx<H: Hash + 'static>(
|
||||
unpin_rx: &UnpinRx<H>,
|
||||
items: impl IntoIterator<Item = H>,
|
||||
) {
|
||||
let expected_hashes = HashSet::<H>::from_iter(items);
|
||||
for i in 0..expected_hashes.len() {
|
||||
let Ok((hash, _)) = unpin_rx.try_recv() else {
|
||||
panic!("Another unpin event is expected, but failed to pull item {i} from channel");
|
||||
};
|
||||
assert!(
|
||||
expected_hashes.contains(&hash),
|
||||
"Hash {hash:?} was unpinned, but is not expected to have been"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An initialized event containing a BlockRef (useful for comparisons)
|
||||
pub fn ev_initialized_ref(n: u64) -> FollowEvent<BlockRef<H256>> {
|
||||
FollowEvent::Initialized(Initialized {
|
||||
finalized_block_hashes: vec![BlockRef::new(H256::from_low_u64_le(n))],
|
||||
finalized_block_runtime: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A new block event containing a BlockRef (useful for comparisons)
|
||||
pub fn ev_new_block_ref(parent: u64, n: u64) -> FollowEvent<BlockRef<H256>> {
|
||||
FollowEvent::NewBlock(NewBlock {
|
||||
parent_block_hash: BlockRef::new(H256::from_low_u64_le(parent)),
|
||||
block_hash: BlockRef::new(H256::from_low_u64_le(n)),
|
||||
new_runtime: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A best block event containing a BlockRef (useful for comparisons)
|
||||
pub fn ev_best_block_ref(n: u64) -> FollowEvent<BlockRef<H256>> {
|
||||
FollowEvent::BestBlockChanged(BestBlockChanged {
|
||||
best_block_hash: BlockRef::new(H256::from_low_u64_le(n)),
|
||||
})
|
||||
}
|
||||
|
||||
/// A finalized event containing a BlockRef (useful for comparisons)
|
||||
pub fn ev_finalized_ref(ns: impl IntoIterator<Item = u64>) -> FollowEvent<BlockRef<H256>> {
|
||||
FollowEvent::Finalized(Finalized {
|
||||
finalized_block_hashes: ns
|
||||
.into_iter()
|
||||
.map(|h| BlockRef::new(H256::from_low_u64_le(h)))
|
||||
.collect(),
|
||||
pruned_block_hashes: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::follow_stream::test_utils::{
|
||||
ev_best_block, ev_finalized, ev_initialized, ev_new_block,
|
||||
};
|
||||
use super::test_utils::{assert_from_unpin_rx, ev_new_block_ref, test_unpin_stream_getter};
|
||||
use super::*;
|
||||
use crate::config::substrate::H256;
|
||||
|
||||
#[tokio::test]
|
||||
async fn hands_back_blocks() {
|
||||
let (follow_unpin, _) = test_unpin_stream_getter(
|
||||
|| {
|
||||
[
|
||||
Ok(ev_new_block(0, 1)),
|
||||
Ok(ev_new_block(1, 2)),
|
||||
Ok(ev_new_block(2, 3)),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
10,
|
||||
);
|
||||
|
||||
let out: Vec<_> = follow_unpin.filter_map(async |e| e.ok()).collect().await;
|
||||
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![
|
||||
FollowStreamMsg::Ready("sub_id_0".into()),
|
||||
FollowStreamMsg::Event(ev_new_block_ref(0, 1)),
|
||||
FollowStreamMsg::Event(ev_new_block_ref(1, 2)),
|
||||
FollowStreamMsg::Event(ev_new_block_ref(2, 3)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unpins_initialized_block() {
|
||||
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|
||||
|| {
|
||||
[
|
||||
Ok(ev_initialized(0)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
3,
|
||||
);
|
||||
|
||||
let _r = follow_unpin.next().await.unwrap().unwrap();
|
||||
|
||||
// Drop the initialized block:
|
||||
let i0 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(i0);
|
||||
|
||||
// Let a finalization event occur.
|
||||
let _f1 = follow_unpin.next().await.unwrap().unwrap();
|
||||
|
||||
// Now, initialized block should be unpinned.
|
||||
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(0)]);
|
||||
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(0)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unpins_old_blocks() {
|
||||
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|
||||
|| {
|
||||
[
|
||||
Ok(ev_initialized(0)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Ok(ev_finalized([2], [])),
|
||||
Ok(ev_finalized([3], [])),
|
||||
Ok(ev_finalized([4], [])),
|
||||
Ok(ev_finalized([5], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
3,
|
||||
);
|
||||
|
||||
let _r = follow_unpin.next().await.unwrap().unwrap();
|
||||
let _i0 = follow_unpin.next().await.unwrap().unwrap();
|
||||
unpin_rx.try_recv().expect_err("nothing unpinned yet");
|
||||
let _f1 = follow_unpin.next().await.unwrap().unwrap();
|
||||
unpin_rx.try_recv().expect_err("nothing unpinned yet");
|
||||
let _f2 = follow_unpin.next().await.unwrap().unwrap();
|
||||
unpin_rx.try_recv().expect_err("nothing unpinned yet");
|
||||
let _f3 = follow_unpin.next().await.unwrap().unwrap();
|
||||
|
||||
// Max age is 3, so after block 3 finalized, block 0 becomes too old and is unpinned.
|
||||
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(0)]);
|
||||
|
||||
let _f4 = follow_unpin.next().await.unwrap().unwrap();
|
||||
|
||||
// Block 1 is now too old and is unpinned.
|
||||
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(1)]);
|
||||
|
||||
let _f5 = follow_unpin.next().await.unwrap().unwrap();
|
||||
|
||||
// Block 2 is now too old and is unpinned.
|
||||
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(2)]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dropped_new_blocks_should_not_get_unpinned_until_finalization() {
|
||||
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|
||||
|| {
|
||||
[
|
||||
Ok(ev_initialized(0)),
|
||||
Ok(ev_new_block(0, 1)),
|
||||
Ok(ev_new_block(1, 2)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Ok(ev_finalized([2], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
10,
|
||||
);
|
||||
|
||||
let _r = follow_unpin.next().await.unwrap().unwrap();
|
||||
let _i0 = follow_unpin.next().await.unwrap().unwrap();
|
||||
|
||||
let n1 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(n1);
|
||||
let n2 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(n2);
|
||||
|
||||
// New blocks dropped but still pinned:
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
|
||||
|
||||
let f1 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(f1);
|
||||
|
||||
// After block 1 finalized, both blocks are still pinned because:
|
||||
// - block 1 was handed back in the finalized event, so will be unpinned next time.
|
||||
// - block 2 wasn't mentioned in the finalized event, so should not have been unpinned yet.
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
|
||||
|
||||
let f2 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(f2);
|
||||
|
||||
// After block 2 finalized, block 1 can be unpinned finally, but block 2 needs to wait one more event.
|
||||
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
|
||||
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(1)]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dropped_new_blocks_should_not_get_unpinned_until_pruned() {
|
||||
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|
||||
|| {
|
||||
[
|
||||
Ok(ev_initialized(0)),
|
||||
Ok(ev_new_block(0, 1)),
|
||||
Ok(ev_new_block(1, 2)),
|
||||
Ok(ev_new_block(1, 3)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Ok(ev_finalized([2], [3])),
|
||||
Ok(ev_finalized([4], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
10,
|
||||
);
|
||||
|
||||
let _r = follow_unpin.next().await.unwrap().unwrap();
|
||||
let _i0 = follow_unpin.next().await.unwrap().unwrap();
|
||||
|
||||
let n1 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(n1);
|
||||
let n2 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(n2);
|
||||
let n3 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(n3);
|
||||
|
||||
let f1 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(f1);
|
||||
|
||||
// After block 1 is finalized, everything is still pinned because the finalization event
|
||||
// itself returns 1, and 2/3 aren't finalized or pruned yet.
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(3)));
|
||||
|
||||
let f2 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(f2);
|
||||
|
||||
// After the next finalization event, block 1 can finally be unpinned since it was Finalized
|
||||
// last event _and_ is no longer handed back anywhere. 2 and 3 should still be pinned.
|
||||
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
|
||||
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(3)));
|
||||
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(1)]);
|
||||
|
||||
let f4 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(f4);
|
||||
|
||||
// After some other finalized event, we are now allowed to ditch the previously pruned and
|
||||
// finalized blocks 2 and 3.
|
||||
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
|
||||
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(3)));
|
||||
assert_from_unpin_rx(
|
||||
&unpin_rx,
|
||||
[H256::from_low_u64_le(2), H256::from_low_u64_le(3)],
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn never_unpin_new_block_before_finalized() {
|
||||
// Ensure that if we drop a new block; the pinning is still active until the block is finalized.
|
||||
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|
||||
|| {
|
||||
[
|
||||
Ok(ev_initialized(0)),
|
||||
Ok(ev_new_block(0, 1)),
|
||||
Ok(ev_new_block(1, 2)),
|
||||
Ok(ev_best_block(1)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Ok(ev_finalized([2], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
]
|
||||
},
|
||||
10,
|
||||
);
|
||||
|
||||
let _r = follow_unpin.next().await.unwrap().unwrap();
|
||||
|
||||
// drop initialised block 0 and new block 1 and new block 2.
|
||||
let i0 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(i0);
|
||||
let n1 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(n1);
|
||||
let n2 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(n2);
|
||||
let b1 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(b1);
|
||||
|
||||
// Nothing unpinned yet!
|
||||
unpin_rx.try_recv().expect_err("nothing unpinned yet");
|
||||
|
||||
let f1 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(f1);
|
||||
|
||||
// After finalization, block 1 is now ready to be unpinned (it won't be seen again),
|
||||
// but isn't actually unpinned yet (because it was just handed back in f1). Block 0
|
||||
// however has now been unpinned.
|
||||
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(0)));
|
||||
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(0)]);
|
||||
unpin_rx.try_recv().expect_err("nothing unpinned yet");
|
||||
|
||||
let f2 = follow_unpin.next().await.unwrap().unwrap();
|
||||
drop(f2);
|
||||
|
||||
// After f2, we can get rid of block 1 now, which was finalized last time.
|
||||
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
|
||||
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(1)]);
|
||||
unpin_rx.try_recv().expect_err("nothing unpinned yet");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,878 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module will expose a backend implementation based on the new APIs
|
||||
//! described at <https://github.com/paritytech/json-rpc-interface-spec/>. See
|
||||
//! [`rpc_methods`] for the raw API calls.
|
||||
//!
|
||||
//! # Warning
|
||||
//!
|
||||
//! Everything in this module is **unstable**, meaning that it could change without
|
||||
//! warning at any time.
|
||||
|
||||
mod follow_stream;
|
||||
mod follow_stream_driver;
|
||||
mod follow_stream_unpin;
|
||||
mod storage_items;
|
||||
|
||||
use self::follow_stream_driver::FollowStreamFinalizedHeads;
|
||||
use crate::backend::{
|
||||
Backend, BlockRef, BlockRefT, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
|
||||
TransactionStatus, utils::retry,
|
||||
};
|
||||
use crate::config::{Config, Hash, HashFor};
|
||||
use crate::error::{BackendError, RpcError};
|
||||
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::task::Poll;
|
||||
use storage_items::StorageItems;
|
||||
use pezkuwi_subxt_rpcs::RpcClient;
|
||||
use pezkuwi_subxt_rpcs::methods::chain_head::{
|
||||
FollowEvent, MethodResponse, RuntimeEvent, StorageQuery, StorageQueryType, StorageResultType,
|
||||
};
|
||||
|
||||
/// Re-export RPC types and methods from [`pezkuwi_subxt_rpcs::methods::chain_head`].
|
||||
pub mod rpc_methods {
|
||||
pub use pezkuwi_subxt_rpcs::methods::legacy::*;
|
||||
}
|
||||
|
||||
// Expose the RPC methods.
|
||||
pub use pezkuwi_subxt_rpcs::methods::chain_head::ChainHeadRpcMethods;
|
||||
|
||||
/// Configure and build an [`ChainHeadBackend`].
|
||||
pub struct ChainHeadBackendBuilder<T> {
|
||||
max_block_life: usize,
|
||||
transaction_timeout_secs: usize,
|
||||
submit_transactions_ignoring_follow_events: bool,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Default for ChainHeadBackendBuilder<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ChainHeadBackendBuilder<T> {
|
||||
/// Create a new [`ChainHeadBackendBuilder`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
max_block_life: usize::MAX,
|
||||
transaction_timeout_secs: 240,
|
||||
submit_transactions_ignoring_follow_events: false,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// The age of a block is defined here as the difference between the current finalized block number
|
||||
/// and the block number of a given block. Once the difference equals or exceeds the number given
|
||||
/// here, the block is unpinned.
|
||||
///
|
||||
/// By default, we will never automatically unpin blocks, but if the number of pinned blocks that we
|
||||
/// keep hold of exceeds the number that the server can tolerate, then a `stop` event is generated and
|
||||
/// we are forced to resubscribe, losing any pinned blocks.
|
||||
pub fn max_block_life(mut self, max_block_life: usize) -> Self {
|
||||
self.max_block_life = max_block_life;
|
||||
self
|
||||
}
|
||||
|
||||
/// When a transaction is submitted, we wait for events indicating it's successfully made it into a finalized
|
||||
/// block. If it takes too long for this to happen, we assume that something went wrong and that we should
|
||||
/// give up waiting.
|
||||
///
|
||||
/// Provide a value here to denote how long, in seconds, to wait before giving up. Defaults to 240 seconds.
|
||||
///
|
||||
/// If [`Self::submit_transactions_ignoring_follow_events()`] is called, this timeout is ignored.
|
||||
pub fn transaction_timeout(mut self, timeout_secs: usize) -> Self {
|
||||
self.transaction_timeout_secs = timeout_secs;
|
||||
self
|
||||
}
|
||||
|
||||
/// When a transaction is submitted, we normally synchronize the events that we get back with events from
|
||||
/// our background `chainHead_follow` subscription, to ensure that any blocks hashes that we see can be
|
||||
/// immediately queried (for example to get events or state at that block), and are kept around unless they
|
||||
/// are no longer needed.
|
||||
///
|
||||
/// The main downside of this synchronization is that there may be a delay in being handed back a
|
||||
/// [`TransactionStatus::InFinalizedBlock`] event while we wait to see the same block hash emitted from
|
||||
/// our background `chainHead_follow` subscription in order to ensure it's available for querying.
|
||||
///
|
||||
/// Calling this method turns off this synchronization, speeding up the response and removing any reliance
|
||||
/// on the `chainHead_follow` subscription continuing to run without stopping throughout submitting a transaction.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This can lead to errors when calling APIs like `wait_for_finalized_success`, which will try to retrieve events
|
||||
/// at the finalized block, because there will be a race and the finalized block may not be available for querying
|
||||
/// yet.
|
||||
pub fn submit_transactions_ignoring_follow_events(mut self) -> Self {
|
||||
self.submit_transactions_ignoring_follow_events = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// A low-level API to build the backend and driver which requires polling the driver for the backend
|
||||
/// to make progress.
|
||||
///
|
||||
/// This is useful if you want to manage the driver yourself, for example if you want to run it in on
|
||||
/// a specific runtime.
|
||||
///
|
||||
/// If you just want to run the driver in the background until completion in on the default runtime,
|
||||
/// use [`ChainHeadBackendBuilder::build_with_background_driver`] instead.
|
||||
pub fn build(
|
||||
self,
|
||||
client: impl Into<RpcClient>,
|
||||
) -> (ChainHeadBackend<T>, ChainHeadBackendDriver<T>) {
|
||||
// Construct the underlying follow_stream layers:
|
||||
let rpc_methods = ChainHeadRpcMethods::new(client.into());
|
||||
let follow_stream =
|
||||
follow_stream::FollowStream::<HashFor<T>>::from_methods(rpc_methods.clone());
|
||||
let follow_stream_unpin =
|
||||
follow_stream_unpin::FollowStreamUnpin::<HashFor<T>>::from_methods(
|
||||
follow_stream,
|
||||
rpc_methods.clone(),
|
||||
self.max_block_life,
|
||||
);
|
||||
let follow_stream_driver = FollowStreamDriver::new(follow_stream_unpin);
|
||||
|
||||
// Wrap these into the backend and driver that we'll expose.
|
||||
let backend = ChainHeadBackend {
|
||||
methods: rpc_methods,
|
||||
follow_handle: follow_stream_driver.handle(),
|
||||
transaction_timeout_secs: self.transaction_timeout_secs,
|
||||
submit_transactions_ignoring_follow_events: self
|
||||
.submit_transactions_ignoring_follow_events,
|
||||
};
|
||||
let driver = ChainHeadBackendDriver {
|
||||
driver: follow_stream_driver,
|
||||
};
|
||||
|
||||
(backend, driver)
|
||||
}
|
||||
|
||||
/// An API to build the backend and driver which will run in the background until completion
|
||||
/// on the default runtime.
|
||||
///
|
||||
/// - On non-wasm targets, this will spawn the driver on `tokio`.
|
||||
/// - On wasm targets, this will spawn the driver on `wasm-bindgen-futures`.
|
||||
#[cfg(feature = "runtime")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
|
||||
pub fn build_with_background_driver(self, client: impl Into<RpcClient>) -> ChainHeadBackend<T> {
|
||||
fn spawn<F: std::future::Future + Send + 'static>(future: F) {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
tokio::spawn(async move {
|
||||
future.await;
|
||||
});
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
future.await;
|
||||
});
|
||||
}
|
||||
|
||||
let (backend, mut driver) = self.build(client);
|
||||
spawn(async move {
|
||||
// NOTE: we need to poll the driver until it's done i.e returns None
|
||||
// to ensure that the backend is shutdown properly.
|
||||
while let Some(res) = driver.next().await {
|
||||
if let Err(err) = res {
|
||||
tracing::debug!(target: "subxt", "chainHead backend error={err}");
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(target: "subxt", "chainHead backend was closed");
|
||||
});
|
||||
|
||||
backend
|
||||
}
|
||||
}
|
||||
|
||||
/// Driver for the [`ChainHeadBackend`]. This must be polled in order for the
|
||||
/// backend to make progress.
|
||||
#[derive(Debug)]
|
||||
pub struct ChainHeadBackendDriver<T: Config> {
|
||||
driver: FollowStreamDriver<HashFor<T>>,
|
||||
}
|
||||
|
||||
impl<T: Config> Stream for ChainHeadBackendDriver<T> {
|
||||
type Item = <FollowStreamDriver<HashFor<T>> as Stream>::Item;
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
self.driver.poll_next_unpin(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// The chainHead backend.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainHeadBackend<T: Config> {
|
||||
// RPC methods we'll want to call:
|
||||
methods: ChainHeadRpcMethods<T>,
|
||||
// A handle to the chainHead_follow subscription:
|
||||
follow_handle: FollowStreamDriverHandle<HashFor<T>>,
|
||||
// How long to wait until giving up on transactions:
|
||||
transaction_timeout_secs: usize,
|
||||
// Don't synchronise blocks with chainHead_follow when submitting txs:
|
||||
submit_transactions_ignoring_follow_events: bool,
|
||||
}
|
||||
|
||||
impl<T: Config> ChainHeadBackend<T> {
|
||||
/// Configure and construct an [`ChainHeadBackend`] and the associated [`ChainHeadBackendDriver`].
|
||||
pub fn builder() -> ChainHeadBackendBuilder<T> {
|
||||
ChainHeadBackendBuilder::new()
|
||||
}
|
||||
|
||||
/// Stream block headers based on the provided filter fn
|
||||
async fn stream_headers<F>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError>
|
||||
where
|
||||
F: Fn(
|
||||
FollowEvent<follow_stream_unpin::BlockRef<HashFor<T>>>,
|
||||
) -> Vec<follow_stream_unpin::BlockRef<HashFor<T>>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
let methods = self.methods.clone();
|
||||
|
||||
let headers =
|
||||
FollowStreamFinalizedHeads::new(self.follow_handle.subscribe(), f).flat_map(move |r| {
|
||||
let methods = methods.clone();
|
||||
|
||||
let (sub_id, block_refs) = match r {
|
||||
Ok(ev) => ev,
|
||||
Err(e) => return Either::Left(futures::stream::once(async { Err(e) })),
|
||||
};
|
||||
|
||||
Either::Right(
|
||||
futures::stream::iter(block_refs).filter_map(move |block_ref| {
|
||||
let methods = methods.clone();
|
||||
let sub_id = sub_id.clone();
|
||||
|
||||
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.into())),
|
||||
};
|
||||
|
||||
Some(Ok((header, block_ref.into())))
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(headers)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Hash + 'static> BlockRefT for follow_stream_unpin::BlockRef<H> {}
|
||||
impl<H: Hash + 'static> From<follow_stream_unpin::BlockRef<H>> for BlockRef<H> {
|
||||
fn from(b: follow_stream_unpin::BlockRef<H>) -> Self {
|
||||
BlockRef::new(b.hash(), b)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> super::sealed::Sealed for ChainHeadBackend<T> {}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
|
||||
async fn storage_fetch_values(
|
||||
&self,
|
||||
keys: Vec<Vec<u8>>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
|
||||
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 stream = storage_items.filter_map(async |val| {
|
||||
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(stream)))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_keys(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<Vec<u8>>, BackendError> {
|
||||
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_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(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
|
||||
retry(|| async {
|
||||
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(async |val| {
|
||||
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<HashFor<T>, BackendError> {
|
||||
retry(|| async {
|
||||
let genesis_hash = self.methods.chainspec_v1_genesis_hash().await?;
|
||||
Ok(genesis_hash)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_header(&self, at: HashFor<T>) -> Result<Option<T::Header>, BackendError> {
|
||||
retry(|| async {
|
||||
let sub_id = get_subscription_id(&self.follow_handle).await?;
|
||||
let header = self.methods.chainhead_v1_header(&sub_id, at).await?;
|
||||
Ok(header)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_body(&self, at: HashFor<T>) -> Result<Option<Vec<Vec<u8>>>, BackendError> {
|
||||
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::LimitReached.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);
|
||||
};
|
||||
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<HashFor<T>>, BackendError> {
|
||||
let next_ref: Option<BlockRef<HashFor<T>>> = self
|
||||
.follow_handle
|
||||
.subscribe()
|
||||
.events()
|
||||
.filter_map(|ev| {
|
||||
let out = match ev {
|
||||
FollowEvent::Initialized(init) => {
|
||||
init.finalized_block_hashes.last().map(|b| b.clone().into())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
std::future::ready(out)
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
||||
next_ref.ok_or_else(|| RpcError::SubscriptionDropped.into())
|
||||
}
|
||||
|
||||
async fn current_runtime_version(&self) -> Result<RuntimeVersion, BackendError> {
|
||||
// Just start a stream of version infos, and return the first value we get from it.
|
||||
let runtime_version = self.stream_runtime_version().await?.next().await;
|
||||
match runtime_version {
|
||||
None => Err(BackendError::Rpc(RpcError::SubscriptionDropped)),
|
||||
Some(Err(e)) => Err(e),
|
||||
Some(Ok(version)) => Ok(version),
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_runtime_version(
|
||||
&self,
|
||||
) -> Result<StreamOfResults<RuntimeVersion>, BackendError> {
|
||||
// Keep track of runtime details announced in new blocks, and then when blocks
|
||||
// are finalized, find the latest of these that has runtime details, and clear the rest.
|
||||
let mut runtimes = HashMap::new();
|
||||
let runtime_stream = self
|
||||
.follow_handle
|
||||
.subscribe()
|
||||
.events()
|
||||
.filter_map(move |ev| {
|
||||
let output = match ev {
|
||||
FollowEvent::Initialized(ev) => {
|
||||
for finalized_block in ev.finalized_block_hashes {
|
||||
runtimes.remove(&finalized_block.hash());
|
||||
}
|
||||
ev.finalized_block_runtime
|
||||
}
|
||||
FollowEvent::NewBlock(ev) => {
|
||||
if let Some(runtime) = ev.new_runtime {
|
||||
runtimes.insert(ev.block_hash.hash(), runtime);
|
||||
}
|
||||
None
|
||||
}
|
||||
FollowEvent::Finalized(ev) => {
|
||||
let next_runtime = {
|
||||
let mut it = ev
|
||||
.finalized_block_hashes
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|h| runtimes.get(&h.hash()).cloned())
|
||||
.peekable();
|
||||
|
||||
let next = it.next();
|
||||
|
||||
if it.peek().is_some() {
|
||||
tracing::warn!(
|
||||
target: "subxt",
|
||||
"Several runtime upgrades in the finalized blocks but only the latest runtime upgrade is returned"
|
||||
);
|
||||
}
|
||||
|
||||
next
|
||||
};
|
||||
|
||||
// Remove finalized and pruned blocks as valid runtime upgrades.
|
||||
for block in ev
|
||||
.finalized_block_hashes
|
||||
.iter()
|
||||
.chain(ev.pruned_block_hashes.iter())
|
||||
{
|
||||
runtimes.remove(&block.hash());
|
||||
}
|
||||
|
||||
next_runtime
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let runtime_event = match output {
|
||||
None => return std::future::ready(None),
|
||||
Some(ev) => ev,
|
||||
};
|
||||
|
||||
let runtime_details = match runtime_event {
|
||||
RuntimeEvent::Invalid(err) => {
|
||||
return std::future::ready(Some(Err(BackendError::Other(format!("Invalid runtime error using chainHead RPCs: {}", err.error)))))
|
||||
}
|
||||
RuntimeEvent::Valid(ev) => ev,
|
||||
};
|
||||
|
||||
let runtime_version = RuntimeVersion {
|
||||
spec_version: runtime_details.spec.spec_version,
|
||||
transaction_version: runtime_details.spec.transaction_version
|
||||
};
|
||||
std::future::ready(Some(Ok(runtime_version)))
|
||||
});
|
||||
|
||||
Ok(StreamOf::new(Box::pin(runtime_stream)))
|
||||
}
|
||||
|
||||
async fn stream_all_block_headers(
|
||||
&self,
|
||||
_hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
// 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) => {
|
||||
vec![ev.block_hash]
|
||||
}
|
||||
_ => vec![],
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stream_best_block_headers(
|
||||
&self,
|
||||
_hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
// 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],
|
||||
_ => vec![],
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stream_finalized_block_headers(
|
||||
&self,
|
||||
_hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
self.stream_headers(|ev| match ev {
|
||||
FollowEvent::Initialized(init) => init.finalized_block_hashes,
|
||||
FollowEvent::Finalized(ev) => ev.finalized_block_hashes,
|
||||
_ => vec![],
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_transaction(
|
||||
&self,
|
||||
extrinsic: &[u8],
|
||||
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
|
||||
// Submit a transaction. This makes no attempt to sync with follow events,
|
||||
async fn submit_transaction_ignoring_follow_events<T: Config>(
|
||||
extrinsic: &[u8],
|
||||
methods: &ChainHeadRpcMethods<T>,
|
||||
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
|
||||
let tx_progress = methods
|
||||
.transactionwatch_v1_submit_and_watch(extrinsic)
|
||||
.await?
|
||||
.map(|ev| {
|
||||
ev.map(|tx_status| {
|
||||
use pezkuwi_subxt_rpcs::methods::chain_head::TransactionStatus as RpcTransactionStatus;
|
||||
match tx_status {
|
||||
RpcTransactionStatus::Validated => TransactionStatus::Validated,
|
||||
RpcTransactionStatus::Broadcasted => TransactionStatus::Broadcasted,
|
||||
RpcTransactionStatus::BestChainBlockIncluded { block: None } => {
|
||||
TransactionStatus::NoLongerInBestBlock
|
||||
},
|
||||
RpcTransactionStatus::BestChainBlockIncluded { block: Some(block) } => {
|
||||
TransactionStatus::InBestBlock { hash: BlockRef::from_hash(block.hash) }
|
||||
},
|
||||
RpcTransactionStatus::Finalized { block } => {
|
||||
TransactionStatus::InFinalizedBlock { hash: BlockRef::from_hash(block.hash) }
|
||||
},
|
||||
RpcTransactionStatus::Error { error } => {
|
||||
TransactionStatus::Error { message: error }
|
||||
},
|
||||
RpcTransactionStatus::Invalid { error } => {
|
||||
TransactionStatus::Invalid { message: error }
|
||||
},
|
||||
RpcTransactionStatus::Dropped { error } => {
|
||||
TransactionStatus::Dropped { message: error }
|
||||
},
|
||||
}
|
||||
}).map_err(Into::into)
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(tx_progress)))
|
||||
}
|
||||
|
||||
// Submit a transaction. This synchronizes with chainHead_follow events to ensure
|
||||
// that block hashes returned are ready to be queried.
|
||||
async fn submit_transaction_tracking_follow_events<T: Config>(
|
||||
extrinsic: &[u8],
|
||||
transaction_timeout_secs: u64,
|
||||
methods: &ChainHeadRpcMethods<T>,
|
||||
follow_handle: &FollowStreamDriverHandle<HashFor<T>>,
|
||||
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
|
||||
// We care about new and finalized block hashes.
|
||||
enum SeenBlockMarker {
|
||||
New,
|
||||
Finalized,
|
||||
}
|
||||
|
||||
// First, subscribe to new blocks.
|
||||
let mut seen_blocks_sub = follow_handle.subscribe().events();
|
||||
|
||||
// Then, submit the transaction.
|
||||
let mut tx_progress = methods
|
||||
.transactionwatch_v1_submit_and_watch(extrinsic)
|
||||
.await?;
|
||||
|
||||
let mut seen_blocks = HashMap::new();
|
||||
let mut done = false;
|
||||
|
||||
// If we see the finalized event, we start waiting until we find a finalized block that
|
||||
// matches, so we can guarantee to return a pinned block hash and be properly in sync
|
||||
// with chainHead_follow.
|
||||
let mut finalized_hash: Option<HashFor<T>> = None;
|
||||
|
||||
// Record the start time so that we can time out if things appear to take too long.
|
||||
let start_instant = web_time::Instant::now();
|
||||
|
||||
// A quick helper to return a generic error.
|
||||
let err_other = |s: &str| Some(Err(BackendError::Other(s.into())));
|
||||
|
||||
// Now we can attempt to associate tx events with pinned blocks.
|
||||
let tx_stream = futures::stream::poll_fn(move |cx| {
|
||||
loop {
|
||||
// Bail early if we're finished; nothing else to do.
|
||||
if done {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
// Bail if we exceed 4 mins; something very likely went wrong.
|
||||
if start_instant.elapsed().as_secs() > transaction_timeout_secs {
|
||||
return Poll::Ready(err_other(
|
||||
"Timeout waiting for the transaction to be finalized",
|
||||
));
|
||||
}
|
||||
|
||||
// Poll for a follow event, and error if the stream has unexpectedly ended.
|
||||
let follow_ev_poll = match seen_blocks_sub.poll_next_unpin(cx) {
|
||||
Poll::Ready(None) => {
|
||||
return Poll::Ready(err_other(
|
||||
"chainHead_follow stream ended unexpectedly",
|
||||
));
|
||||
}
|
||||
Poll::Ready(Some(follow_ev)) => Poll::Ready(follow_ev),
|
||||
Poll::Pending => Poll::Pending,
|
||||
};
|
||||
let follow_ev_is_pending = follow_ev_poll.is_pending();
|
||||
|
||||
// If there was a follow event, then handle it and loop around to see if there are more.
|
||||
// We want to buffer follow events until we hit Pending, so that we are as up-to-date as possible
|
||||
// for when we see a BestBlockChanged event, so that we have the best change of already having
|
||||
// seen the block that it mentions and returning a proper pinned block.
|
||||
if let Poll::Ready(follow_ev) = follow_ev_poll {
|
||||
match follow_ev {
|
||||
FollowEvent::NewBlock(ev) => {
|
||||
// Optimization: once we have a `finalized_hash`, we only care about finalized
|
||||
// block refs now and can avoid bothering to save new blocks.
|
||||
if finalized_hash.is_none() {
|
||||
seen_blocks.insert(
|
||||
ev.block_hash.hash(),
|
||||
(SeenBlockMarker::New, ev.block_hash),
|
||||
);
|
||||
}
|
||||
}
|
||||
FollowEvent::Finalized(ev) => {
|
||||
for block_ref in ev.finalized_block_hashes {
|
||||
seen_blocks.insert(
|
||||
block_ref.hash(),
|
||||
(SeenBlockMarker::Finalized, block_ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
FollowEvent::Stop => {
|
||||
// If we get this event, we'll lose all of our existing pinned blocks and have a gap
|
||||
// in which we may lose the finalized block that the TX is in. For now, just error if
|
||||
// this happens, to prevent the case in which we never see a finalized block and wait
|
||||
// forever.
|
||||
return Poll::Ready(err_other(
|
||||
"chainHead_follow emitted 'stop' event during transaction submission",
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have a finalized hash, we are done looking for tx events and we are just waiting
|
||||
// for a pinned block with a matching hash (which must appear eventually given it's finalized).
|
||||
if let Some(hash) = &finalized_hash {
|
||||
if let Some((SeenBlockMarker::Finalized, block_ref)) =
|
||||
seen_blocks.remove(hash)
|
||||
{
|
||||
// Found it! Hand back the event with a pinned block. We're done.
|
||||
done = true;
|
||||
let ev = TransactionStatus::InFinalizedBlock {
|
||||
hash: block_ref.into(),
|
||||
};
|
||||
return Poll::Ready(Some(Ok(ev)));
|
||||
} else {
|
||||
// Not found it! If follow ev is pending, then return pending here and wait for
|
||||
// a new one to come in, else loop around and see if we get another one immediately.
|
||||
seen_blocks.clear();
|
||||
if follow_ev_is_pending {
|
||||
return Poll::Pending;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a finalized block yet, we keep polling for tx progress events.
|
||||
let tx_progress_ev = match tx_progress.poll_next_unpin(cx) {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(None) => {
|
||||
return Poll::Ready(err_other(
|
||||
"No more transaction progress events, but we haven't seen a Finalized one yet",
|
||||
));
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e.into()))),
|
||||
Poll::Ready(Some(Ok(ev))) => ev,
|
||||
};
|
||||
|
||||
// When we get one, map it to the correct format (or for finalized ev, wait for the pinned block):
|
||||
use pezkuwi_subxt_rpcs::methods::chain_head::TransactionStatus as RpcTransactionStatus;
|
||||
let tx_progress_ev = match tx_progress_ev {
|
||||
RpcTransactionStatus::Finalized { block } => {
|
||||
// We'll wait until we have seen this hash, to try to guarantee
|
||||
// that when we return this event, the corresponding block is
|
||||
// pinned and accessible.
|
||||
finalized_hash = Some(block.hash);
|
||||
continue;
|
||||
}
|
||||
RpcTransactionStatus::BestChainBlockIncluded { block: Some(block) } => {
|
||||
// Look up a pinned block ref if we can, else return a non-pinned
|
||||
// block that likely isn't accessible. We have no guarantee that a best
|
||||
// block on the node a tx was sent to will ever be known about on the
|
||||
// chainHead_follow subscription.
|
||||
let block_ref = match seen_blocks.get(&block.hash) {
|
||||
Some((_, block_ref)) => block_ref.clone().into(),
|
||||
None => BlockRef::from_hash(block.hash),
|
||||
};
|
||||
TransactionStatus::InBestBlock { hash: block_ref }
|
||||
}
|
||||
RpcTransactionStatus::BestChainBlockIncluded { block: None } => {
|
||||
TransactionStatus::NoLongerInBestBlock
|
||||
}
|
||||
RpcTransactionStatus::Broadcasted => TransactionStatus::Broadcasted,
|
||||
RpcTransactionStatus::Dropped { error, .. } => {
|
||||
TransactionStatus::Dropped { message: error }
|
||||
}
|
||||
RpcTransactionStatus::Error { error } => {
|
||||
TransactionStatus::Error { message: error }
|
||||
}
|
||||
RpcTransactionStatus::Invalid { error } => {
|
||||
TransactionStatus::Invalid { message: error }
|
||||
}
|
||||
RpcTransactionStatus::Validated => TransactionStatus::Validated,
|
||||
};
|
||||
return Poll::Ready(Some(Ok(tx_progress_ev)));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(tx_stream)))
|
||||
}
|
||||
|
||||
if self.submit_transactions_ignoring_follow_events {
|
||||
submit_transaction_ignoring_follow_events(extrinsic, &self.methods).await
|
||||
} else {
|
||||
submit_transaction_tracking_follow_events::<T>(
|
||||
extrinsic,
|
||||
self.transaction_timeout_secs as u64,
|
||||
&self.methods,
|
||||
&self.follow_handle,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<Vec<u8>, BackendError> {
|
||||
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::LimitReached.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);
|
||||
};
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to obtain a subscription ID.
|
||||
async fn get_subscription_id<H: Hash>(
|
||||
follow_handle: &FollowStreamDriverHandle<H>,
|
||||
) -> Result<String, BackendError> {
|
||||
let Some(sub_id) = follow_handle.subscribe().subscription_id().await else {
|
||||
return Err(RpcError::SubscriptionDropped.into());
|
||||
};
|
||||
|
||||
Ok(sub_id)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::follow_stream_driver::FollowStreamDriverHandle;
|
||||
use super::follow_stream_unpin::BlockRef;
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::error::{BackendError, RpcError};
|
||||
use futures::{FutureExt, Stream, StreamExt};
|
||||
use std::collections::VecDeque;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use pezkuwi_subxt_rpcs::methods::chain_head::{
|
||||
ChainHeadRpcMethods, FollowEvent, MethodResponse, StorageQuery, StorageResult,
|
||||
};
|
||||
|
||||
/// Obtain a stream of storage items given some query. this handles continuing
|
||||
/// and stopping under the hood, and returns a stream of `StorageResult`s.
|
||||
pub struct StorageItems<T: Config> {
|
||||
done: bool,
|
||||
operation_id: Arc<str>,
|
||||
buffered_responses: VecDeque<StorageResult>,
|
||||
continue_call: ContinueFutGetter,
|
||||
continue_fut: Option<ContinueFut>,
|
||||
follow_event_stream: FollowEventStream<HashFor<T>>,
|
||||
}
|
||||
|
||||
impl<T: Config> StorageItems<T> {
|
||||
// Subscribe to follow events, and return a stream of storage results
|
||||
// given some storage queries. The stream will automatically resume as
|
||||
// needed, and stop when done.
|
||||
pub async fn from_methods(
|
||||
queries: impl Iterator<Item = StorageQuery<&[u8]>>,
|
||||
at: HashFor<T>,
|
||||
follow_handle: &FollowStreamDriverHandle<HashFor<T>>,
|
||||
methods: ChainHeadRpcMethods<T>,
|
||||
) -> Result<Self, BackendError> {
|
||||
let sub_id = super::get_subscription_id(follow_handle).await?;
|
||||
|
||||
// Subscribe to events and make the initial request to get an operation ID.
|
||||
let follow_events = follow_handle.subscribe().events();
|
||||
let status = methods
|
||||
.chainhead_v1_storage(&sub_id, at, queries, None)
|
||||
.await?;
|
||||
let operation_id: Arc<str> = match status {
|
||||
MethodResponse::LimitReached => return Err(RpcError::LimitReached.into()),
|
||||
MethodResponse::Started(s) => s.operation_id.into(),
|
||||
};
|
||||
|
||||
// A function which returns the call to continue the subscription:
|
||||
let continue_call: ContinueFutGetter = {
|
||||
let operation_id = operation_id.clone();
|
||||
Box::new(move || {
|
||||
let sub_id = sub_id.clone();
|
||||
let operation_id = operation_id.clone();
|
||||
let methods = methods.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
methods
|
||||
.chainhead_v1_continue(&sub_id, &operation_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
Ok(StorageItems::new(
|
||||
operation_id,
|
||||
continue_call,
|
||||
Box::pin(follow_events),
|
||||
))
|
||||
}
|
||||
|
||||
fn new(
|
||||
operation_id: Arc<str>,
|
||||
continue_call: ContinueFutGetter,
|
||||
follow_event_stream: FollowEventStream<HashFor<T>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
done: false,
|
||||
buffered_responses: VecDeque::new(),
|
||||
operation_id,
|
||||
continue_call,
|
||||
continue_fut: None,
|
||||
follow_event_stream,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type FollowEventStream<Hash> =
|
||||
Pin<Box<dyn Stream<Item = FollowEvent<BlockRef<Hash>>> + Send + 'static>>;
|
||||
pub type ContinueFutGetter = Box<dyn Fn() -> ContinueFut + Send + 'static>;
|
||||
pub type ContinueFut = Pin<Box<dyn Future<Output = Result<(), BackendError>> + Send + 'static>>;
|
||||
|
||||
impl<T: Config> Stream for StorageItems<T> {
|
||||
type Item = Result<StorageResult, BackendError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
if self.done {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
if let Some(item) = self.buffered_responses.pop_front() {
|
||||
return Poll::Ready(Some(Ok(item)));
|
||||
}
|
||||
|
||||
if let Some(mut fut) = self.continue_fut.take() {
|
||||
match fut.poll_unpin(cx) {
|
||||
Poll::Pending => {
|
||||
self.continue_fut = Some(fut);
|
||||
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)));
|
||||
}
|
||||
Poll::Ready(Ok(())) => {
|
||||
// Finished; carry on.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ev = match self.follow_event_stream.poll_next_unpin(cx) {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Ready(Some(ev)) => ev,
|
||||
};
|
||||
|
||||
match ev {
|
||||
FollowEvent::OperationWaitingForContinue(id)
|
||||
if id.operation_id == *self.operation_id =>
|
||||
{
|
||||
// Start a call to ask for more events
|
||||
self.continue_fut = Some((self.continue_call)());
|
||||
continue;
|
||||
}
|
||||
FollowEvent::OperationStorageDone(id) if id.operation_id == *self.operation_id => {
|
||||
// We're finished!
|
||||
self.done = true;
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
FollowEvent::OperationStorageItems(items)
|
||||
if items.operation_id == *self.operation_id =>
|
||||
{
|
||||
// We have items; buffer them to emit next loops.
|
||||
self.buffered_responses = items.items;
|
||||
continue;
|
||||
}
|
||||
FollowEvent::OperationError(err) if err.operation_id == *self.operation_id => {
|
||||
// Something went wrong obtaining storage items; mark as done and return the error.
|
||||
self.done = true;
|
||||
return Poll::Ready(Some(Err(BackendError::Other(err.error))));
|
||||
}
|
||||
_ => {
|
||||
// We don't care about this event; wait for the next.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+662
@@ -0,0 +1,662 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module exposes a legacy backend implementation, which relies
|
||||
//! on the legacy RPC API methods.
|
||||
|
||||
use self::rpc_methods::TransactionStatus as RpcTransactionStatus;
|
||||
use crate::backend::utils::{retry, retry_stream};
|
||||
use crate::backend::{
|
||||
Backend, BlockRef, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
|
||||
TransactionStatus,
|
||||
};
|
||||
use crate::config::{Config, HashFor, Header};
|
||||
use crate::error::BackendError;
|
||||
use async_trait::async_trait;
|
||||
use futures::TryStreamExt;
|
||||
use futures::{Future, FutureExt, Stream, StreamExt, future, future::Either, stream};
|
||||
use std::collections::VecDeque;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use pezkuwi_subxt_rpcs::RpcClient;
|
||||
|
||||
/// Re-export legacy RPC types and methods from [`pezkuwi_subxt_rpcs::methods::legacy`].
|
||||
pub mod rpc_methods {
|
||||
pub use pezkuwi_subxt_rpcs::methods::legacy::*;
|
||||
}
|
||||
|
||||
// Expose the RPC methods.
|
||||
pub use rpc_methods::LegacyRpcMethods;
|
||||
|
||||
/// Configure and build an [`LegacyBackend`].
|
||||
pub struct LegacyBackendBuilder<T> {
|
||||
storage_page_size: u32,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Default for LegacyBackendBuilder<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> LegacyBackendBuilder<T> {
|
||||
/// Create a new [`LegacyBackendBuilder`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
storage_page_size: 64,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterating over storage entries using the [`LegacyBackend`] requires
|
||||
/// fetching entries in batches. This configures the number of entries that
|
||||
/// we'll try to obtain in each batch (default: 64).
|
||||
pub fn storage_page_size(mut self, storage_page_size: u32) -> Self {
|
||||
self.storage_page_size = storage_page_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Given an [`RpcClient`] to use to make requests, this returns a [`LegacyBackend`],
|
||||
/// which implements the [`Backend`] trait.
|
||||
pub fn build(self, client: impl Into<RpcClient>) -> LegacyBackend<T> {
|
||||
LegacyBackend {
|
||||
storage_page_size: self.storage_page_size,
|
||||
methods: LegacyRpcMethods::new(client.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The legacy backend.
|
||||
#[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> {
|
||||
LegacyBackendBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> super::sealed::Sealed for LegacyBackend<T> {}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
async fn storage_fetch_values(
|
||||
&self,
|
||||
keys: Vec<Vec<u8>>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
|
||||
fn get_entry<T: Config>(
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
methods: LegacyRpcMethods<T>,
|
||||
) -> impl Future<Output = Result<Option<StorageResponse>, BackendError>> {
|
||||
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| get_entry(key, at, methods.clone()));
|
||||
|
||||
let s = stream::iter(iter)
|
||||
// Resolve the future
|
||||
.then(|fut| fut)
|
||||
// Filter any Options out (ie if we didn't find a value at some key we return nothing for it).
|
||||
.filter_map(|r| future::ready(r.transpose()));
|
||||
|
||||
Ok(StreamOf(Box::pin(s)))
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_keys(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<Vec<u8>>, BackendError> {
|
||||
let keys = StorageFetchDescendantKeysStream {
|
||||
at,
|
||||
key,
|
||||
storage_page_size: self.storage_page_size,
|
||||
methods: self.methods.clone(),
|
||||
done: Default::default(),
|
||||
keys_fut: Default::default(),
|
||||
pagination_start_key: None,
|
||||
};
|
||||
|
||||
let keys = keys.flat_map(|keys| {
|
||||
match keys {
|
||||
Err(e) => {
|
||||
// If there's an error, return that next:
|
||||
Either::Left(stream::iter(std::iter::once(Err(e))))
|
||||
}
|
||||
Ok(keys) => {
|
||||
// Or, stream each "ok" value:
|
||||
Either::Right(stream::iter(keys.into_iter().map(Ok)))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(keys)))
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_values(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
|
||||
let keys_stream = StorageFetchDescendantKeysStream {
|
||||
at,
|
||||
key,
|
||||
storage_page_size: self.storage_page_size,
|
||||
methods: self.methods.clone(),
|
||||
done: Default::default(),
|
||||
keys_fut: Default::default(),
|
||||
pagination_start_key: None,
|
||||
};
|
||||
|
||||
Ok(StreamOf(Box::pin(StorageFetchDescendantValuesStream {
|
||||
keys: keys_stream,
|
||||
results_fut: None,
|
||||
results: Default::default(),
|
||||
})))
|
||||
}
|
||||
|
||||
async fn genesis_hash(&self) -> Result<HashFor<T>, BackendError> {
|
||||
retry(|| async {
|
||||
let hash = self.methods.genesis_hash().await?;
|
||||
Ok(hash)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_header(&self, at: HashFor<T>) -> Result<Option<T::Header>, BackendError> {
|
||||
retry(|| async {
|
||||
let header = self.methods.chain_get_header(Some(at)).await?;
|
||||
Ok(header)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_body(&self, at: HashFor<T>) -> Result<Option<Vec<Vec<u8>>>, BackendError> {
|
||||
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<HashFor<T>>, BackendError> {
|
||||
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, BackendError> {
|
||||
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>, BackendError> {
|
||||
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_err(|e| e.into()).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 mut keep = true;
|
||||
if let Err(e) = r {
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
keep = false;
|
||||
}
|
||||
}
|
||||
async move { keep }
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(stream)))
|
||||
}
|
||||
|
||||
async fn stream_all_block_headers(
|
||||
&self,
|
||||
hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
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_err(|e| e.into()).map(move |r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash_with(hasher);
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(retry_sub)
|
||||
}
|
||||
|
||||
async fn stream_best_block_headers(
|
||||
&self,
|
||||
hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
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_err(|e| e.into()).map(move |r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash_with(hasher);
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(retry_sub)
|
||||
}
|
||||
|
||||
async fn stream_finalized_block_headers(
|
||||
&self,
|
||||
hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
let this = self.clone();
|
||||
|
||||
let retry_sub = retry_stream(move || {
|
||||
let this = this.clone();
|
||||
Box::pin(async move {
|
||||
let sub = this.methods.chain_subscribe_finalized_heads().await?;
|
||||
|
||||
// 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(move |r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash_with(hasher);
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(sub)))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(retry_sub)
|
||||
}
|
||||
|
||||
async fn submit_transaction(
|
||||
&self,
|
||||
extrinsic: &[u8],
|
||||
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
|
||||
let sub = self
|
||||
.methods
|
||||
.author_submit_and_watch_extrinsic(extrinsic)
|
||||
.await?;
|
||||
|
||||
let sub = sub.filter_map(|r| {
|
||||
let mapped = r
|
||||
.map_err(|e| e.into())
|
||||
.map(|tx| {
|
||||
match tx {
|
||||
// We ignore these because they don't map nicely to the new API. They don't signal "end states" so this should be fine.
|
||||
RpcTransactionStatus::Future => None,
|
||||
RpcTransactionStatus::Retracted(_) => None,
|
||||
// These roughly map across:
|
||||
RpcTransactionStatus::Ready => Some(TransactionStatus::Validated),
|
||||
RpcTransactionStatus::Broadcast(_peers) => {
|
||||
Some(TransactionStatus::Broadcasted)
|
||||
}
|
||||
RpcTransactionStatus::InBlock(hash) => {
|
||||
Some(TransactionStatus::InBestBlock {
|
||||
hash: BlockRef::from_hash(hash),
|
||||
})
|
||||
}
|
||||
// These 5 mean that the stream will very likely end:
|
||||
RpcTransactionStatus::FinalityTimeout(_) => {
|
||||
Some(TransactionStatus::Dropped {
|
||||
message: "Finality timeout".into(),
|
||||
})
|
||||
}
|
||||
RpcTransactionStatus::Finalized(hash) => {
|
||||
Some(TransactionStatus::InFinalizedBlock {
|
||||
hash: BlockRef::from_hash(hash),
|
||||
})
|
||||
}
|
||||
RpcTransactionStatus::Usurped(_) => Some(TransactionStatus::Invalid {
|
||||
message: "Transaction was usurped by another with the same nonce"
|
||||
.into(),
|
||||
}),
|
||||
RpcTransactionStatus::Dropped => Some(TransactionStatus::Dropped {
|
||||
message: "Transaction was dropped".into(),
|
||||
}),
|
||||
RpcTransactionStatus::Invalid => Some(TransactionStatus::Invalid {
|
||||
message:
|
||||
"Transaction is invalid (eg because of a bad nonce, signature etc)"
|
||||
.into(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.transpose();
|
||||
|
||||
future::ready(mapped)
|
||||
});
|
||||
|
||||
Ok(StreamOf::new(Box::pin(sub)))
|
||||
}
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<Vec<u8>, BackendError> {
|
||||
retry(|| async {
|
||||
let res = self
|
||||
.methods
|
||||
.state_call(method, call_parameters, Some(at))
|
||||
.await?;
|
||||
Ok(res)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: This is exposed for testing but is not considered stable and may change
|
||||
/// without notice in a patch release.
|
||||
#[doc(hidden)]
|
||||
pub fn subscribe_to_block_headers_filling_in_gaps<T, S, E>(
|
||||
methods: LegacyRpcMethods<T>,
|
||||
sub: S,
|
||||
mut last_block_num: Option<u64>,
|
||||
) -> impl Stream<Item = Result<T::Header, BackendError>> + Send
|
||||
where
|
||||
T: Config,
|
||||
S: Stream<Item = Result<T::Header, E>> + Send,
|
||||
E: Into<BackendError> + Send + 'static,
|
||||
{
|
||||
sub.flat_map(move |s| {
|
||||
// Get the header, or return a stream containing just the error.
|
||||
let header = match s {
|
||||
Ok(header) => header,
|
||||
Err(e) => return Either::Left(stream::once(async { Err(e.into()) })),
|
||||
};
|
||||
|
||||
// We want all previous details up to, but not including this current block num.
|
||||
let end_block_num = header.number().into();
|
||||
|
||||
// This is one after the last block we returned details for last time.
|
||||
let start_block_num = last_block_num.map(|n| n + 1).unwrap_or(end_block_num);
|
||||
|
||||
// Iterate over all of the previous blocks we need headers for, ignoring the current block
|
||||
// (which we already have the header info for):
|
||||
let methods = methods.clone();
|
||||
let previous_headers = stream::iter(start_block_num..end_block_num)
|
||||
.then(move |n| {
|
||||
let methods = methods.clone();
|
||||
async move {
|
||||
let hash = methods.chain_get_block_hash(Some(n.into())).await?;
|
||||
let header = methods.chain_get_header(hash).await?;
|
||||
Ok::<_, BackendError>(header)
|
||||
}
|
||||
})
|
||||
.filter_map(async |h| h.transpose());
|
||||
|
||||
// On the next iteration, we'll get details starting just after this end block.
|
||||
last_block_num = Some(end_block_num);
|
||||
|
||||
// Return a combination of any previous headers plus the new header.
|
||||
Either::Right(previous_headers.chain(stream::once(async { Ok(header) })))
|
||||
})
|
||||
}
|
||||
|
||||
/// This provides a stream of values given some prefix `key`. It
|
||||
/// internally manages pagination and such.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct StorageFetchDescendantKeysStream<T: Config> {
|
||||
methods: LegacyRpcMethods<T>,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
// How many entries to ask for each time.
|
||||
storage_page_size: u32,
|
||||
// What key do we start paginating from? None = from the beginning.
|
||||
pagination_start_key: Option<Vec<u8>>,
|
||||
// Keys, future and cached:
|
||||
keys_fut:
|
||||
Option<Pin<Box<dyn Future<Output = Result<Vec<Vec<u8>>, BackendError>> + Send + 'static>>>,
|
||||
// Set to true when we're done:
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl<T: Config> std::marker::Unpin for StorageFetchDescendantKeysStream<T> {}
|
||||
|
||||
impl<T: Config> Stream for StorageFetchDescendantKeysStream<T> {
|
||||
type Item = Result<Vec<Vec<u8>>, BackendError>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut();
|
||||
loop {
|
||||
// We're already done.
|
||||
if this.done {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
// Poll future to fetch next keys.
|
||||
if let Some(mut keys_fut) = this.keys_fut.take() {
|
||||
let Poll::Ready(keys) = keys_fut.poll_unpin(cx) else {
|
||||
this.keys_fut = Some(keys_fut);
|
||||
return Poll::Pending;
|
||||
};
|
||||
|
||||
match keys {
|
||||
Ok(mut keys) => {
|
||||
if this.pagination_start_key.is_some()
|
||||
&& keys.first() == this.pagination_start_key.as_ref()
|
||||
{
|
||||
// Currently, Smoldot returns the "start key" as the first key in the input
|
||||
// (see https://github.com/smol-dot/smoldot/issues/1692), whereas Substrate doesn't.
|
||||
// We don't expect the start key to be returned either (since it was the last key of prev
|
||||
// iteration), so remove it if we see it. This `remove()` method isn't very efficient but
|
||||
// this will be a non issue with the RPC V2 APIs or if Smoldot aligns with Substrate anyway.
|
||||
keys.remove(0);
|
||||
}
|
||||
if keys.is_empty() {
|
||||
// No keys left; we're done!
|
||||
this.done = true;
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
// The last key is where we want to paginate from next time.
|
||||
this.pagination_start_key = keys.last().cloned();
|
||||
// return all of the keys from this run.
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Else, we don't have a fut to get keys yet so start one going.
|
||||
let methods = this.methods.clone();
|
||||
let key = this.key.clone();
|
||||
let at = this.at;
|
||||
let storage_page_size = this.storage_page_size;
|
||||
let pagination_start_key = this.pagination_start_key.clone();
|
||||
let keys_fut = async move {
|
||||
let keys = methods
|
||||
.state_get_keys_paged(
|
||||
&key,
|
||||
storage_page_size,
|
||||
pagination_start_key.as_deref(),
|
||||
Some(at),
|
||||
)
|
||||
.await?;
|
||||
Ok(keys)
|
||||
};
|
||||
this.keys_fut = Some(Box::pin(keys_fut));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This provides a stream of values given some stream of keys.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct StorageFetchDescendantValuesStream<T: Config> {
|
||||
// Stream of keys.
|
||||
keys: StorageFetchDescendantKeysStream<T>,
|
||||
// Then we track the future to get the values back for each key:
|
||||
results_fut: Option<
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<Output = Result<Option<VecDeque<(Vec<u8>, Vec<u8>)>>, BackendError>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
// And finally we return each result back one at a time:
|
||||
results: VecDeque<(Vec<u8>, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl<T: Config> Stream for StorageFetchDescendantValuesStream<T> {
|
||||
type Item = Result<StorageResponse, BackendError>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut();
|
||||
loop {
|
||||
// If we have results back, return them one by one
|
||||
if let Some((key, value)) = this.results.pop_front() {
|
||||
let res = StorageResponse { key, value };
|
||||
return Poll::Ready(Some(Ok(res)));
|
||||
}
|
||||
|
||||
// If we're waiting on the next results then poll that future:
|
||||
if let Some(mut results_fut) = this.results_fut.take() {
|
||||
match results_fut.poll_unpin(cx) {
|
||||
Poll::Ready(Ok(Some(results))) => {
|
||||
this.results = results;
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Ok(None)) => {
|
||||
// No values back for some keys? Skip.
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Some(Err(e))),
|
||||
Poll::Pending => {
|
||||
this.results_fut = Some(results_fut);
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match this.keys.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Ok(keys))) => {
|
||||
let methods = this.keys.methods.clone();
|
||||
let at = this.keys.at;
|
||||
let results_fut = async move {
|
||||
let keys = keys.iter().map(|k| &**k);
|
||||
let values = retry(|| async {
|
||||
let res = methods
|
||||
.state_query_storage_at(keys.clone(), Some(at))
|
||||
.await?;
|
||||
Ok(res)
|
||||
})
|
||||
.await?;
|
||||
let values: VecDeque<_> = values
|
||||
.into_iter()
|
||||
.flat_map(|v| {
|
||||
v.changes.into_iter().filter_map(|(k, v)| {
|
||||
let v = v?;
|
||||
Some((k.0, v.0))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(Some(values))
|
||||
};
|
||||
|
||||
this.results_fut = Some(Box::pin(results_fut));
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1072
File diff suppressed because it is too large
Load Diff
+276
@@ -0,0 +1,276 @@
|
||||
//! RPC utils.
|
||||
|
||||
use super::{StreamOf, StreamOfResults};
|
||||
use crate::error::BackendError;
|
||||
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>, BackendError>> + Send>>;
|
||||
|
||||
pub(crate) enum PendingOrStream<T> {
|
||||
Pending(BoxFuture<'static, Result<StreamOfResults<T>, BackendError>>),
|
||||
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, BackendError>;
|
||||
|
||||
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
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// use subxt::backend::utils::retry;
|
||||
///
|
||||
/// async fn some_future() -> Result<(), subxt::error::BackendError> {
|
||||
/// 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, BackendError>
|
||||
where
|
||||
F: FnMut() -> T,
|
||||
T: Future<Output = Result<R, BackendError>>,
|
||||
{
|
||||
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, in the event of a disconnection,
|
||||
// we may not get the correct subscription ID back on reconnecting.
|
||||
//
|
||||
// This is because we have a race between this future and the
|
||||
// separate chainHead subscription, which runs in a different task.
|
||||
// if this future is too quick, it'll be given back an old
|
||||
// subscription ID from the chainHead subscription which has yet
|
||||
// to reconnect and establish a new subscription ID.
|
||||
//
|
||||
// In the event of a wrong subscription Id being used, we happen to
|
||||
// hand back an `RpcError::LimitReached`, and so can retry when we
|
||||
// specifically hit that error to see if we get a new subscription ID
|
||||
// eventually.
|
||||
if e.is_rpc_limit_reached() && 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
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// 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>, BackendError>
|
||||
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() -> BackendError {
|
||||
BackendError::Rpc(pezkuwi_subxt_rpcs::Error::DisconnectedWillReconnect(String::new()).into())
|
||||
}
|
||||
|
||||
fn custom_err() -> BackendError {
|
||||
BackendError::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, BackendError>>>()
|
||||
.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(|| 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(|| 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, BackendError::Other(_))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2019-2025 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::BlockRef,
|
||||
blocks::Extrinsics,
|
||||
client::{OfflineClientT, OnlineClientT},
|
||||
config::{Config, HashFor, Header},
|
||||
error::{AccountNonceError, BlockError, EventsError, ExtrinsicError},
|
||||
events,
|
||||
runtime_api::RuntimeApi,
|
||||
storage::StorageClientAt,
|
||||
};
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use futures::lock::Mutex as AsyncMutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A representation of a block.
|
||||
pub struct Block<T: Config, C> {
|
||||
header: T::Header,
|
||||
block_ref: BlockRef<HashFor<T>>,
|
||||
client: C,
|
||||
// Since we obtain the same events for every extrinsic, let's
|
||||
// cache them so that we only ever do that once:
|
||||
cached_events: CachedEvents<T>,
|
||||
}
|
||||
|
||||
impl<T: Config, C: Clone> Clone for Block<T, C> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
header: self.header.clone(),
|
||||
block_ref: self.block_ref.clone(),
|
||||
client: self.client.clone(),
|
||||
cached_events: self.cached_events.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A cache for our events so we don't fetch them more than once when
|
||||
// iterating over events for extrinsics.
|
||||
pub(crate) type CachedEvents<T> = Arc<AsyncMutex<Option<events::Events<T>>>>;
|
||||
|
||||
impl<T, C> Block<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OfflineClientT<T>,
|
||||
{
|
||||
pub(crate) fn new(header: T::Header, block_ref: BlockRef<HashFor<T>>, client: C) -> Self {
|
||||
Block {
|
||||
header,
|
||||
block_ref,
|
||||
client,
|
||||
cached_events: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a reference to the given block. While this reference is kept alive,
|
||||
/// the backend will (if possible) endeavour to keep hold of the block.
|
||||
pub fn reference(&self) -> BlockRef<HashFor<T>> {
|
||||
self.block_ref.clone()
|
||||
}
|
||||
|
||||
/// Return the block hash.
|
||||
pub fn hash(&self) -> HashFor<T> {
|
||||
self.block_ref.hash()
|
||||
}
|
||||
|
||||
/// Return the block number.
|
||||
pub fn number(&self) -> <T::Header as crate::config::Header>::Number {
|
||||
self.header().number()
|
||||
}
|
||||
|
||||
/// Return the entire block header.
|
||||
pub fn header(&self) -> &T::Header {
|
||||
&self.header
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> Block<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientT<T>,
|
||||
{
|
||||
/// Return the events associated with the block, fetching them from the node if necessary.
|
||||
pub async fn events(&self) -> Result<events::Events<T>, EventsError> {
|
||||
get_events(&self.client, self.hash(), &self.cached_events).await
|
||||
}
|
||||
|
||||
/// Fetch and return the extrinsics in the block body.
|
||||
pub async fn extrinsics(&self) -> Result<Extrinsics<T, C>, ExtrinsicError> {
|
||||
let block_hash = self.hash();
|
||||
|
||||
let extrinsics = self
|
||||
.client
|
||||
.backend()
|
||||
.block_body(block_hash)
|
||||
.await
|
||||
.map_err(ExtrinsicError::CannotGetBlockBody)?
|
||||
.ok_or_else(|| ExtrinsicError::BlockNotFound(block_hash.into()))?;
|
||||
|
||||
let extrinsics = Extrinsics::new(
|
||||
self.client.clone(),
|
||||
extrinsics,
|
||||
self.cached_events.clone(),
|
||||
block_hash,
|
||||
)?;
|
||||
|
||||
Ok(extrinsics)
|
||||
}
|
||||
|
||||
/// Work with storage.
|
||||
pub fn storage(&self) -> StorageClientAt<T, C> {
|
||||
StorageClientAt::new(self.client.clone(), self.block_ref.clone())
|
||||
}
|
||||
|
||||
/// Execute a runtime API call at this block.
|
||||
pub async fn runtime_api(&self) -> RuntimeApi<T, C> {
|
||||
RuntimeApi::new(self.client.clone(), self.block_ref.clone())
|
||||
}
|
||||
|
||||
/// Get the account nonce for a given account ID at this block.
|
||||
pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result<u64, BlockError> {
|
||||
get_account_nonce(&self.client, account_id, self.hash())
|
||||
.await
|
||||
.map_err(|e| BlockError::AccountNonceError {
|
||||
block_hash: self.hash().into(),
|
||||
account_id: account_id.encode().into(),
|
||||
reason: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Return Events from the cache, or fetch from the node if needed.
|
||||
pub(crate) async fn get_events<C, T>(
|
||||
client: &C,
|
||||
block_hash: HashFor<T>,
|
||||
cached_events: &AsyncMutex<Option<events::Events<T>>>,
|
||||
) -> Result<events::Events<T>, EventsError>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientT<T>,
|
||||
{
|
||||
// Acquire lock on the events cache. We either get back our events or we fetch and set them
|
||||
// before unlocking, so only one fetch call should ever be made. We do this because the
|
||||
// same events can be shared across all extrinsics in the block.
|
||||
let mut lock = cached_events.lock().await;
|
||||
let events = match &*lock {
|
||||
Some(events) => events.clone(),
|
||||
None => {
|
||||
let events = events::EventsClient::new(client.clone())
|
||||
.at(block_hash)
|
||||
.await?;
|
||||
lock.replace(events.clone());
|
||||
events
|
||||
}
|
||||
};
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
// Return the account nonce at some block hash for an account ID.
|
||||
pub(crate) async fn get_account_nonce<C, T>(
|
||||
client: &C,
|
||||
account_id: &T::AccountId,
|
||||
block_hash: HashFor<T>,
|
||||
) -> Result<u64, AccountNonceError>
|
||||
where
|
||||
C: OnlineClientT<T>,
|
||||
T: Config,
|
||||
{
|
||||
let account_nonce_bytes = client
|
||||
.backend()
|
||||
.call(
|
||||
"AccountNonceApi_account_nonce",
|
||||
Some(&account_id.encode()),
|
||||
block_hash,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// custom decoding from a u16/u32/u64 into a u64, based on the number of bytes we got back.
|
||||
let cursor = &mut &account_nonce_bytes[..];
|
||||
let account_nonce: u64 = match account_nonce_bytes.len() {
|
||||
2 => u16::decode(cursor)?.into(),
|
||||
4 => u32::decode(cursor)?.into(),
|
||||
8 => u64::decode(cursor)?,
|
||||
_ => {
|
||||
return Err(AccountNonceError::WrongNumberOfBytes(
|
||||
account_nonce_bytes.len(),
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(account_nonce)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::Block;
|
||||
use crate::{
|
||||
backend::{BlockRef, StreamOfResults},
|
||||
client::OnlineClientT,
|
||||
config::{Config, HashFor},
|
||||
error::BlockError,
|
||||
utils::PhantomDataSendSync,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use futures::StreamExt;
|
||||
use std::future::Future;
|
||||
|
||||
type BlockStream<T> = StreamOfResults<T>;
|
||||
type BlockStreamRes<T> = Result<BlockStream<T>, BlockError>;
|
||||
|
||||
/// A client for working with blocks.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct BlocksClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: PhantomDataSendSync<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> BlocksClient<T, Client> {
|
||||
/// Create a new [`BlocksClient`].
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: PhantomDataSendSync::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> BlocksClient<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Obtain block details given the provided block hash.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This call only supports blocks produced since the most recent
|
||||
/// runtime upgrade. You can attempt to retrieve older blocks,
|
||||
/// but may run into errors attempting to work with them.
|
||||
pub fn at(
|
||||
&self,
|
||||
block_ref: impl Into<BlockRef<HashFor<T>>>,
|
||||
) -> impl Future<Output = Result<Block<T, Client>, BlockError>> + Send + 'static {
|
||||
self.at_or_latest(Some(block_ref.into()))
|
||||
}
|
||||
|
||||
/// Obtain block details of the latest finalized block.
|
||||
pub fn at_latest(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<Block<T, Client>, BlockError>> + Send + 'static {
|
||||
self.at_or_latest(None)
|
||||
}
|
||||
|
||||
/// Obtain block details given the provided block hash, or the latest block if `None` is
|
||||
/// provided.
|
||||
fn at_or_latest(
|
||||
&self,
|
||||
block_ref: Option<BlockRef<HashFor<T>>>,
|
||||
) -> impl Future<Output = Result<Block<T, Client>, BlockError>> + Send + 'static {
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
// If a block ref isn't provided, we'll get the latest finalized ref to use.
|
||||
let block_ref = match block_ref {
|
||||
Some(r) => r,
|
||||
None => client
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(BlockError::CouldNotGetLatestBlock)?,
|
||||
};
|
||||
|
||||
let maybe_block_header = client
|
||||
.backend()
|
||||
.block_header(block_ref.hash())
|
||||
.await
|
||||
.map_err(|e| BlockError::CouldNotGetBlockHeader {
|
||||
block_hash: block_ref.hash().into(),
|
||||
reason: e,
|
||||
})?;
|
||||
|
||||
let block_header = match maybe_block_header {
|
||||
Some(header) => header,
|
||||
None => {
|
||||
return Err(BlockError::BlockNotFound {
|
||||
block_hash: block_ref.hash().into(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Block::new(block_header, block_ref, client))
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to all new blocks imported by the node.
|
||||
///
|
||||
/// **Note:** You probably want to use [`Self::subscribe_finalized()`] most of
|
||||
/// the time.
|
||||
pub fn subscribe_all(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<BlockStream<Block<T, Client>>, BlockError>> + Send + 'static
|
||||
where
|
||||
Client: Send + Sync + 'static,
|
||||
{
|
||||
let client = self.client.clone();
|
||||
let hasher = client.hasher();
|
||||
header_sub_fut_to_block_sub(self.clone(), async move {
|
||||
let stream = client
|
||||
.backend()
|
||||
.stream_all_block_headers(hasher)
|
||||
.await
|
||||
.map_err(BlockError::CouldNotSubscribeToAllBlocks)?;
|
||||
BlockStreamRes::Ok(stream)
|
||||
})
|
||||
}
|
||||
|
||||
/// Subscribe to all new blocks imported by the node onto the current best fork.
|
||||
///
|
||||
/// **Note:** You probably want to use [`Self::subscribe_finalized()`] most of
|
||||
/// the time.
|
||||
pub fn subscribe_best(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<BlockStream<Block<T, Client>>, BlockError>> + Send + 'static
|
||||
where
|
||||
Client: Send + Sync + 'static,
|
||||
{
|
||||
let client = self.client.clone();
|
||||
let hasher = client.hasher();
|
||||
header_sub_fut_to_block_sub(self.clone(), async move {
|
||||
let stream = client
|
||||
.backend()
|
||||
.stream_best_block_headers(hasher)
|
||||
.await
|
||||
.map_err(BlockError::CouldNotSubscribeToBestBlocks)?;
|
||||
BlockStreamRes::Ok(stream)
|
||||
})
|
||||
}
|
||||
|
||||
/// Subscribe to finalized blocks.
|
||||
pub fn subscribe_finalized(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<BlockStream<Block<T, Client>>, BlockError>> + Send + 'static
|
||||
where
|
||||
Client: Send + Sync + 'static,
|
||||
{
|
||||
let client = self.client.clone();
|
||||
let hasher = client.hasher();
|
||||
header_sub_fut_to_block_sub(self.clone(), async move {
|
||||
let stream = client
|
||||
.backend()
|
||||
.stream_finalized_block_headers(hasher)
|
||||
.await
|
||||
.map_err(BlockError::CouldNotSubscribeToFinalizedBlocks)?;
|
||||
BlockStreamRes::Ok(stream)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a promise that will return a subscription to some block headers,
|
||||
/// and return a subscription to some blocks based on this.
|
||||
async fn header_sub_fut_to_block_sub<T, Client, S>(
|
||||
blocks_client: BlocksClient<T, Client>,
|
||||
sub: S,
|
||||
) -> Result<BlockStream<Block<T, Client>>, BlockError>
|
||||
where
|
||||
T: Config,
|
||||
S: Future<Output = Result<BlockStream<(T::Header, BlockRef<HashFor<T>>)>, BlockError>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
Client: OnlineClientT<T> + Send + Sync + 'static,
|
||||
{
|
||||
let sub = sub.await?.then(move |header_and_ref| {
|
||||
let client = blocks_client.client.clone();
|
||||
async move {
|
||||
let (header, block_ref) = match header_and_ref {
|
||||
Ok(header_and_ref) => header_and_ref,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
Ok(Block::new(header, block_ref, client))
|
||||
}
|
||||
});
|
||||
BlockStreamRes::Ok(StreamOfResults::new(Box::pin(sub)))
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::{
|
||||
blocks::block_types::{CachedEvents, get_events},
|
||||
client::{OfflineClientT, OnlineClientT},
|
||||
config::{Config, HashFor},
|
||||
error::{EventsError, ExtrinsicDecodeErrorAt, ExtrinsicError},
|
||||
events,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use scale_decode::{DecodeAsFields, DecodeAsType};
|
||||
use pezkuwi_subxt_core::blocks::{ExtrinsicDetails as CoreExtrinsicDetails, Extrinsics as CoreExtrinsics};
|
||||
|
||||
// Re-export anything that's directly returned/used in the APIs below.
|
||||
pub use pezkuwi_subxt_core::blocks::{
|
||||
ExtrinsicTransactionExtension, ExtrinsicTransactionExtensions, StaticExtrinsic,
|
||||
};
|
||||
|
||||
/// The body of a block.
|
||||
pub struct Extrinsics<T: Config, C> {
|
||||
inner: CoreExtrinsics<T>,
|
||||
client: C,
|
||||
cached_events: CachedEvents<T>,
|
||||
hash: HashFor<T>,
|
||||
}
|
||||
|
||||
impl<T, C> Extrinsics<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OfflineClientT<T>,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
client: C,
|
||||
extrinsics: Vec<Vec<u8>>,
|
||||
cached_events: CachedEvents<T>,
|
||||
hash: HashFor<T>,
|
||||
) -> Result<Self, ExtrinsicDecodeErrorAt> {
|
||||
let inner = CoreExtrinsics::decode_from(extrinsics, client.metadata())?;
|
||||
Ok(Self {
|
||||
inner,
|
||||
client,
|
||||
cached_events,
|
||||
hash,
|
||||
})
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::Extrinsics::len()`].
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::Extrinsics::is_empty()`].
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// Return the block hash that these extrinsics are from.
|
||||
pub fn block_hash(&self) -> HashFor<T> {
|
||||
self.hash
|
||||
}
|
||||
|
||||
/// Returns an iterator over the extrinsics in the block body.
|
||||
// Dev note: The returned iterator is 'static + Send so that we can box it up and make
|
||||
// use of it with our `FilterExtrinsic` stuff.
|
||||
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicDetails<T, C>> + Send + Sync + 'static {
|
||||
let client = self.client.clone();
|
||||
let cached_events = self.cached_events.clone();
|
||||
let block_hash = self.hash;
|
||||
|
||||
self.inner.iter().map(move |inner| {
|
||||
ExtrinsicDetails::new(inner, client.clone(), block_hash, cached_events.clone())
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate through the extrinsics using metadata to dynamically decode and skip
|
||||
/// them, and return only those which should decode to the provided `E` type.
|
||||
/// If an error occurs, all subsequent iterations return `None`.
|
||||
pub fn find<E: StaticExtrinsic>(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<FoundExtrinsic<T, C, E>, ExtrinsicError>> {
|
||||
self.inner.find::<E>().map(|res| {
|
||||
match res {
|
||||
Err(e) => Err(ExtrinsicError::from(e)),
|
||||
Ok(ext) => {
|
||||
// Wrap details from subxt-core into what we want here:
|
||||
let details = ExtrinsicDetails::new(
|
||||
ext.details,
|
||||
self.client.clone(),
|
||||
self.hash,
|
||||
self.cached_events.clone(),
|
||||
);
|
||||
|
||||
Ok(FoundExtrinsic {
|
||||
details,
|
||||
value: ext.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate through the extrinsics using metadata to dynamically decode and skip
|
||||
/// them, and return the first extrinsic found which decodes to the provided `E` type.
|
||||
pub fn find_first<E: StaticExtrinsic>(
|
||||
&self,
|
||||
) -> Result<Option<FoundExtrinsic<T, C, E>>, ExtrinsicError> {
|
||||
self.find::<E>().next().transpose()
|
||||
}
|
||||
|
||||
/// Iterate through the extrinsics using metadata to dynamically decode and skip
|
||||
/// them, and return the last extrinsic found which decodes to the provided `Ev` type.
|
||||
pub fn find_last<E: StaticExtrinsic>(
|
||||
&self,
|
||||
) -> Result<Option<FoundExtrinsic<T, C, E>>, ExtrinsicError> {
|
||||
self.find::<E>().last().transpose()
|
||||
}
|
||||
|
||||
/// Find an extrinsics that decodes to the type provided. Returns true if it was found.
|
||||
pub fn has<E: StaticExtrinsic>(&self) -> Result<bool, ExtrinsicError> {
|
||||
Ok(self.find::<E>().next().transpose()?.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
/// A single extrinsic in a block.
|
||||
pub struct ExtrinsicDetails<T: Config, C> {
|
||||
inner: CoreExtrinsicDetails<T>,
|
||||
/// The block hash of this extrinsic (needed to fetch events).
|
||||
block_hash: HashFor<T>,
|
||||
/// Subxt client.
|
||||
client: C,
|
||||
/// Cached events.
|
||||
cached_events: CachedEvents<T>,
|
||||
}
|
||||
|
||||
impl<T, C> ExtrinsicDetails<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OfflineClientT<T>,
|
||||
{
|
||||
// Attempt to dynamically decode a single extrinsic from the given input.
|
||||
pub(crate) fn new(
|
||||
inner: CoreExtrinsicDetails<T>,
|
||||
client: C,
|
||||
block_hash: HashFor<T>,
|
||||
cached_events: CachedEvents<T>,
|
||||
) -> ExtrinsicDetails<T, C> {
|
||||
ExtrinsicDetails {
|
||||
inner,
|
||||
client,
|
||||
block_hash,
|
||||
cached_events,
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::hash()`].
|
||||
pub fn hash(&self) -> HashFor<T> {
|
||||
self.inner.hash()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::is_signed()`].
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.inner.is_signed()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::index()`].
|
||||
pub fn index(&self) -> u32 {
|
||||
self.inner.index()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::bytes()`].
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
self.inner.bytes()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::call_bytes()`].
|
||||
pub fn call_bytes(&self) -> &[u8] {
|
||||
self.inner.call_bytes()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::field_bytes()`].
|
||||
pub fn field_bytes(&self) -> &[u8] {
|
||||
self.inner.field_bytes()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::address_bytes()`].
|
||||
pub fn address_bytes(&self) -> Option<&[u8]> {
|
||||
self.inner.address_bytes()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::signature_bytes()`].
|
||||
pub fn signature_bytes(&self) -> Option<&[u8]> {
|
||||
self.inner.signature_bytes()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::transaction_extensions_bytes()`].
|
||||
pub fn transaction_extensions_bytes(&self) -> Option<&[u8]> {
|
||||
self.inner.transaction_extensions_bytes()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::transaction_extensions()`].
|
||||
pub fn transaction_extensions(&self) -> Option<ExtrinsicTransactionExtensions<'_, T>> {
|
||||
self.inner.transaction_extensions()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::pallet_index()`].
|
||||
pub fn pallet_index(&self) -> u8 {
|
||||
self.inner.pallet_index()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::call_index()`].
|
||||
pub fn call_index(&self) -> u8 {
|
||||
self.inner.call_index()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::pallet_name()`].
|
||||
pub fn pallet_name(&self) -> &str {
|
||||
self.inner.pallet_name()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::call_name()`].
|
||||
pub fn call_name(&self) -> &str {
|
||||
self.inner.call_name()
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::decode_as_fields()`].
|
||||
pub fn decode_as_fields<E: DecodeAsFields>(&self) -> Result<E, ExtrinsicError> {
|
||||
self.inner.decode_as_fields().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::as_extrinsic()`].
|
||||
pub fn as_extrinsic<E: StaticExtrinsic>(&self) -> Result<Option<E>, ExtrinsicError> {
|
||||
self.inner.as_extrinsic::<E>().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::as_root_extrinsic()`].
|
||||
pub fn as_root_extrinsic<E: DecodeAsType>(&self) -> Result<E, ExtrinsicError> {
|
||||
self.inner.as_root_extrinsic::<E>().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> ExtrinsicDetails<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientT<T>,
|
||||
{
|
||||
/// The events associated with the extrinsic.
|
||||
pub async fn events(&self) -> Result<ExtrinsicEvents<T>, EventsError> {
|
||||
let events = get_events(&self.client, self.block_hash, &self.cached_events).await?;
|
||||
let ext_hash = self.inner.hash();
|
||||
Ok(ExtrinsicEvents::new(ext_hash, self.index(), events))
|
||||
}
|
||||
}
|
||||
|
||||
/// A Static Extrinsic found in a block coupled with it's details.
|
||||
pub struct FoundExtrinsic<T: Config, C, E> {
|
||||
/// Details for the extrinsic.
|
||||
pub details: ExtrinsicDetails<T, C>,
|
||||
/// The decoded extrinsic value.
|
||||
pub value: E,
|
||||
}
|
||||
|
||||
/// The events associated with a given extrinsic.
|
||||
#[derive_where(Debug)]
|
||||
pub struct ExtrinsicEvents<T: Config> {
|
||||
// The hash of the extrinsic (handy to expose here because
|
||||
// this type is returned from TxProgress things in the most
|
||||
// basic flows, so it's the only place people can access it
|
||||
// without complicating things for themselves).
|
||||
ext_hash: HashFor<T>,
|
||||
// The index of the extrinsic:
|
||||
idx: u32,
|
||||
// All of the events in the block:
|
||||
events: events::Events<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicEvents<T> {
|
||||
/// Creates a new instance of `ExtrinsicEvents`.
|
||||
#[doc(hidden)]
|
||||
pub fn new(ext_hash: HashFor<T>, idx: u32, events: events::Events<T>) -> Self {
|
||||
Self {
|
||||
ext_hash,
|
||||
idx,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// The index of the extrinsic that these events are produced from.
|
||||
pub fn extrinsic_index(&self) -> u32 {
|
||||
self.idx
|
||||
}
|
||||
|
||||
/// Return the hash of the extrinsic.
|
||||
pub fn extrinsic_hash(&self) -> HashFor<T> {
|
||||
self.ext_hash
|
||||
}
|
||||
|
||||
/// Return all of the events in the block that the extrinsic is in.
|
||||
pub fn all_events_in_block(&self) -> &events::Events<T> {
|
||||
&self.events
|
||||
}
|
||||
|
||||
/// Iterate over all of the raw events associated with this transaction.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::iter()`] does, with the
|
||||
/// exception that it filters out events not related to the submitted extrinsic.
|
||||
pub fn iter(&self) -> impl Iterator<Item = Result<events::EventDetails<T>, EventsError>> {
|
||||
self.events.iter().filter(|ev| {
|
||||
ev.as_ref()
|
||||
.map(|ev| ev.phase() == events::Phase::ApplyExtrinsic(self.idx))
|
||||
.unwrap_or(true) // Keep any errors.
|
||||
})
|
||||
}
|
||||
|
||||
/// Find all of the transaction events matching the event type provided as a generic parameter.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::find()`] does, with the
|
||||
/// exception that it filters out events not related to the submitted extrinsic.
|
||||
pub fn find<Ev: events::StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, EventsError>> {
|
||||
self.iter()
|
||||
.filter_map(|ev| ev.and_then(|ev| ev.as_event::<Ev>()).transpose())
|
||||
}
|
||||
|
||||
/// Iterate through the transaction events using metadata to dynamically decode and skip
|
||||
/// them, and return the first event found which decodes to the provided `Ev` type.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::find_first()`] does, with the
|
||||
/// exception that it ignores events not related to the submitted extrinsic.
|
||||
pub fn find_first<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, EventsError> {
|
||||
self.find::<Ev>().next().transpose()
|
||||
}
|
||||
|
||||
/// Iterate through the transaction events using metadata to dynamically decode and skip
|
||||
/// them, and return the last event found which decodes to the provided `Ev` type.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::find_last()`] does, with the
|
||||
/// exception that it ignores events not related to the submitted extrinsic.
|
||||
pub fn find_last<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, EventsError> {
|
||||
self.find::<Ev>().last().transpose()
|
||||
}
|
||||
|
||||
/// Find an event in those associated with this transaction. Returns true if it was found.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::has()`] does, with the
|
||||
/// exception that it ignores events not related to the submitted extrinsic.
|
||||
pub fn has<Ev: events::StaticEvent>(&self) -> Result<bool, EventsError> {
|
||||
Ok(self.find::<Ev>().next().transpose()?.is_some())
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module exposes the necessary functionality for working with events.
|
||||
|
||||
mod block_types;
|
||||
mod blocks_client;
|
||||
mod extrinsic_types;
|
||||
|
||||
/// A reference to a block.
|
||||
pub use crate::backend::BlockRef;
|
||||
|
||||
pub use block_types::Block;
|
||||
pub use blocks_client::BlocksClient;
|
||||
pub use extrinsic_types::{
|
||||
ExtrinsicDetails, ExtrinsicEvents, ExtrinsicTransactionExtension,
|
||||
ExtrinsicTransactionExtensions, Extrinsics, FoundExtrinsic, StaticExtrinsic,
|
||||
};
|
||||
|
||||
// We get account nonce info in tx_client, too, so re-use the logic:
|
||||
pub(crate) use block_types::get_account_nonce;
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
// Dev note; I used the following command to normalize and wrap comments:
|
||||
// rustfmt +nightly --config wrap_comments=true,comment_width=100,normalize_comments=true subxt/src/book/custom_values
|
||||
// It messed up comments in code blocks though, so be prepared to go and fix those.
|
||||
|
||||
//! # The Subxt Guide
|
||||
//!
|
||||
//! Subxt is a library for interacting with Substrate based nodes. It has a focus on **sub**mitting
|
||||
//! e**xt**rinsics, hence the name, however it's also capable of reading blocks, storage, events and
|
||||
//! constants from a node. The aim of this guide is to explain key concepts and get you started with
|
||||
//! using Subxt.
|
||||
//!
|
||||
//! 1. [Features](#features-at-a-glance)
|
||||
//! 2. [Limitations](#limitations)
|
||||
//! 3. [Quick start](#quick-start)
|
||||
//! 4. [Usage](#usage)
|
||||
//!
|
||||
//! ## Features at a glance
|
||||
//!
|
||||
//! Here's a quick overview of the features that Subxt has to offer:
|
||||
//!
|
||||
//! - Subxt allows you to generate a static, type safe interface to a node given some metadata; this
|
||||
//! allows you to catch many errors at compile time rather than runtime.
|
||||
//! - Subxt also makes heavy use of node metadata to encode/decode the data sent to/from it. This
|
||||
//! allows it to target almost any node which can output the correct metadata, and allows it some
|
||||
//! flexibility in encoding and decoding things to account for cross-node differences.
|
||||
//! - Subxt has a pallet-oriented interface, meaning that code you write to talk to some pallet on
|
||||
//! one node will often "Just Work" when pointed at different nodes that use the same pallet.
|
||||
//! - Subxt can work offline; you can generate and sign transactions, access constants from node
|
||||
//! metadata and more, without a network connection. This is all checked at compile time, so you
|
||||
//! can be certain it won't try to establish a network connection if you don't want it to.
|
||||
//! - Subxt can forego the statically generated interface and build transactions, storage queries
|
||||
//! and constant queries using data provided at runtime, rather than queries constructed
|
||||
//! statically.
|
||||
//! - Subxt can be compiled to WASM to run in the browser, allowing it to back Rust based browser
|
||||
//! apps, or even bind to JS apps.
|
||||
//!
|
||||
//! ## Limitations
|
||||
//!
|
||||
//! In various places, you can provide a block hash to access data at a particular block, for
|
||||
//! instance:
|
||||
//!
|
||||
//! - [`crate::storage::StorageClient::at`]
|
||||
//! - [`crate::events::EventsClient::at`]
|
||||
//! - [`crate::blocks::BlocksClient::at`]
|
||||
//! - [`crate::runtime_api::RuntimeApiClient::at`]
|
||||
//!
|
||||
//! However, Subxt is (by default) only capable of properly working with blocks that were produced
|
||||
//! after the most recent runtime update. This is because it uses the most recent metadata given
|
||||
//! back by a node to encode and decode things. It's possible to decode older blocks produced by a
|
||||
//! runtime that emits compatible (currently, V14) metadata by manually setting the metadata used by
|
||||
//! the client using [`crate::client::OnlineClient::set_metadata()`].
|
||||
//!
|
||||
//! Subxt does not support working with blocks produced prior to the runtime update that introduces
|
||||
//! V14 metadata. It may have some success decoding older blocks using newer metadata, but may also
|
||||
//! completely fail to do so.
|
||||
//!
|
||||
//! ## Quick start
|
||||
//!
|
||||
//! Here is a simple but complete example of using Subxt to transfer some tokens from the example
|
||||
//! accounts, Alice to Bob:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../examples/tx_basic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! This example assumes that a Polkadot node is running locally (Subxt endeavors to support all
|
||||
//! recent releases). Typically, to use Subxt to talk to some custom Substrate node (for example a
|
||||
//! parachain node), you'll want to:
|
||||
//!
|
||||
//! 1. [Generate an interface](setup::codegen)
|
||||
//! 2. [Create a config](setup::config)
|
||||
//! 3. [Use the config to instantiate the client](setup::client)
|
||||
//!
|
||||
//! Follow the above links to learn more about each step.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! Once Subxt is configured, the next step is interacting with a node. Follow the links
|
||||
//! below to learn more about how to use Subxt for each of the following things:
|
||||
//!
|
||||
//! - [Transactions](usage::transactions): Subxt can build and submit transactions, wait until they are in
|
||||
//! blocks, and retrieve the associated events.
|
||||
//! - [Storage](usage::storage): Subxt can query the node storage.
|
||||
//! - [Events](usage::events): Subxt can read the events emitted for recent blocks.
|
||||
//! - [Constants](usage::constants): Subxt can access the constant values stored in a node, which
|
||||
//! remain the same for a given runtime version.
|
||||
//! - [Blocks](usage::blocks): Subxt can load recent blocks or subscribe to new/finalized blocks,
|
||||
//! reading the extrinsics, events and storage at these blocks.
|
||||
//! - [Runtime APIs](usage::runtime_apis): Subxt can make calls into pallet runtime APIs to retrieve
|
||||
//! data.
|
||||
//! - [Custom values](usage::custom_values): Subxt can access "custom values" stored in the metadata.
|
||||
//! - [Raw RPC calls](usage::rpc): Subxt can be used to make raw RPC requests to compatible nodes.
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! Some complete, self contained examples which are not a part of this guide:
|
||||
//!
|
||||
//! - [`parachain-example`](https://github.com/paritytech/subxt/tree/master/examples/parachain-example) is an example
|
||||
//! which uses Zombienet to spawn a parachain locally, and then connects to it using Subxt.
|
||||
//! - [`wasm-example`](https://github.com/paritytech/subxt/tree/master/examples/wasm-example) is an example of writing
|
||||
//! a Rust app that contains a Yew based UI, uses Subxt to interact with a chain, and compiles to WASM in order to
|
||||
//! run entirely in the browser.
|
||||
pub mod setup;
|
||||
pub mod usage;
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # The Subxt client.
|
||||
//!
|
||||
//! The client forms the entry point to all of the Subxt APIs. Every client implements one or
|
||||
//! both of [`crate::client::OfflineClientT`] and [`crate::client::OnlineClientT`].
|
||||
//!
|
||||
//! Subxt ships with three clients which implement one or both of traits:
|
||||
//! - An [online client](crate::client::OnlineClient).
|
||||
//! - An [offline client](crate::client::OfflineClient).
|
||||
//! - A light client (which is currently still unstable).
|
||||
//!
|
||||
//! In theory it's possible for users to implement their own clients, although this isn't generally
|
||||
//! expected.
|
||||
//!
|
||||
//! The provided clients are all generic over the [`crate::config::Config`] that they accept, which
|
||||
//! determines how they will interact with the chain.
|
||||
//!
|
||||
//! In the case of the [`crate::OnlineClient`], we have various ways to instantiate it:
|
||||
//!
|
||||
//! - [`crate::OnlineClient::new()`] to connect to a node running locally. This uses the default Subxt
|
||||
//! backend, and the default RPC client.
|
||||
//! - [`crate::OnlineClient::from_url()`] to connect to a node at a specific URL. This uses the default Subxt
|
||||
//! backend, and the default RPC client.
|
||||
//! - [`crate::OnlineClient::from_rpc_client()`] to instantiate the client with a [`crate::backend::rpc::RpcClient`].
|
||||
//! - [`crate::OnlineClient::from_backend()`] to instantiate Subxt using a custom backend. Currently there
|
||||
//! is just one backend, [`crate::backend::legacy::LegacyBackend`]. This backend can be instantiated from
|
||||
//! a [`crate::backend::rpc::RpcClient`].
|
||||
//!
|
||||
//! [`crate::backend::rpc::RpcClient`] can itself be instantiated from anything that implements the low level
|
||||
//! [`crate::backend::rpc::RpcClientT`] trait; this allows you to decide how Subxt will attempt to talk to a node
|
||||
//! if you'd prefer something other default client. We use this approach under the hood to implement the light client.
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! Most of the other examples will instantiate a client. Here are a couple of examples for less common
|
||||
//! cases.
|
||||
//!
|
||||
//! ### Writing a custom [`crate::backend::rpc::RpcClientT`] implementation:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/setup_client_custom_rpc.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Creating an [`crate::OfflineClient`]:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/setup_client_offline.rs")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Generating an interface
|
||||
//!
|
||||
//! The simplest way to use Subxt is to generate an interface to a chain that you'd like to interact
|
||||
//! with. This generated interface allows you to build transactions and construct queries to access
|
||||
//! data while leveraging the full type safety of the Rust compiler.
|
||||
//!
|
||||
//! ## The `#[subxt]` macro
|
||||
//!
|
||||
//! The most common way to generate the interface is to use the [`#[subxt]`](crate::subxt) macro.
|
||||
//! Using this macro looks something like:
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_tiny.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//! ```
|
||||
//!
|
||||
//! The macro takes a path to some node metadata, and uses that to generate the interface you'll use
|
||||
//! to talk to it. [Go here](crate::subxt) to learn more about the options available to the macro.
|
||||
//!
|
||||
//! To obtain this metadata you'll need for the above, you can use the `subxt` CLI tool to download it
|
||||
//! from a node. The tool can be installed via `cargo`:
|
||||
//!
|
||||
//! ```shell
|
||||
//! cargo install subxt-cli
|
||||
//! ```
|
||||
//!
|
||||
//! And then it can be used to fetch metadata and save it to a file:
|
||||
//!
|
||||
//! ```shell
|
||||
//! # Download and save all of the metadata:
|
||||
//! subxt metadata > metadata.scale
|
||||
//! # Download and save only the pallets you want to generate an interface for:
|
||||
//! subxt metadata --pallets Balances,System > metadata.scale
|
||||
//! ```
|
||||
//!
|
||||
//! Explicitly specifying pallets will cause the tool to strip out all unnecessary metadata and type
|
||||
//! information, making the bundle much smaller in the event that you only need to generate an
|
||||
//! interface for a subset of the available pallets on the node.
|
||||
//!
|
||||
//! ## The CLI tool
|
||||
//!
|
||||
//! 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).
|
||||
//! - 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.
|
||||
//!
|
||||
//! If these are an issue, you can manually generate the same code that the macro generates under the hood
|
||||
//! by using the `subxt codegen` command:
|
||||
//!
|
||||
//! ```shell
|
||||
//! # Install the CLI tool if you haven't already:
|
||||
//! cargo install subxt-cli
|
||||
//! # Generate and format rust code, saving it to `interface.rs`:
|
||||
//! subxt codegen | rustfmt > interface.rs
|
||||
//! ```
|
||||
//!
|
||||
//! Use `subxt codegen --help` for more options; many of the options available via the macro are
|
||||
//! also available via the CLI tool, such as the ability to substitute generated types for others,
|
||||
//! or strip out docs from the generated code.
|
||||
//!
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
//! # Creating a Config
|
||||
//!
|
||||
//! Subxt requires you to provide a type implementing [`crate::config::Config`] in order to connect to a node.
|
||||
//! The [`crate::config::Config`] trait for the most part mimics the `frame_system::Config` trait.
|
||||
//! For most use cases, you can just use one of the following Configs shipped with Subxt:
|
||||
//!
|
||||
//! - [`PolkadotConfig`](crate::config::PolkadotConfig) for talking to Polkadot nodes, and
|
||||
//! - [`SubstrateConfig`](crate::config::SubstrateConfig) for talking to generic nodes built with Substrate.
|
||||
//!
|
||||
//! # How to create a Config for a custom chain?
|
||||
//!
|
||||
//! Some chains may use config that is not compatible with our [`PolkadotConfig`](crate::config::PolkadotConfig) or
|
||||
//! [`SubstrateConfig`](crate::config::SubstrateConfig).
|
||||
//!
|
||||
//! We now walk through creating a custom [`crate::config::Config`] for a parachain, using the
|
||||
//! ["Statemint"](https://parachains.info/details/statemint) parachain, also known as "Asset Hub", as an example. It
|
||||
//! is currently (as of 2023-06-26) deployed on Polkadot and [Kusama (as "Statemine")](https://parachains.info/details/statemine).
|
||||
//!
|
||||
//! To construct a valid [`crate::config::Config`] implementation, we need to find out which types to use for `AccountId`, `Hasher`, etc.
|
||||
//! For this, we need to take a look at the source code of Statemint, which is currently a part of the [Cumulus Github repository](https://github.com/paritytech/cumulus).
|
||||
//! The crate defining the asset hub runtime can be found [here](https://github.com/paritytech/cumulus/tree/master/parachains/runtimes/assets/asset-hub-polkadot).
|
||||
//!
|
||||
//! ## `AccountId`, `Hash`, `Hasher` and `Header`
|
||||
//!
|
||||
//! For these config types, we need to find out where the parachain runtime implements the `frame_system::Config` trait.
|
||||
//! Look for a code fragment like `impl frame_system::Config for Runtime { ... }` In the source code.
|
||||
//! For Statemint it looks like [this](https://github.com/paritytech/cumulus/blob/e2b7ad2061824f490c08df27a922c64f50accd6b/parachains/runtimes/assets/asset-hub-polkadot/src/lib.rs#L179)
|
||||
//! at the time of writing. The `AccountId`, `Hash` and `Header` types of the [frame_system::pallet::Config](https://docs.rs/frame-system/latest/frame_system/pallet/trait.Config.html)
|
||||
//! correspond to the ones we want to use in our Subxt [crate::Config]. In the Case of Statemint (Asset Hub) they are:
|
||||
//!
|
||||
//! - AccountId: `sp_core::crypto::AccountId32`
|
||||
//! - Hash: `sp_core::H256`
|
||||
//! - Hasher (type `Hashing` in [frame_system::pallet::Config](https://docs.rs/frame-system/latest/frame_system/pallet/trait.Config.html)): `sp_runtime::traits::BlakeTwo256`
|
||||
//! - Header: `sp_runtime::generic::Header<u32, sp_runtime::traits::BlakeTwo256>`
|
||||
//!
|
||||
//! Subxt has its own versions of some of these types in order to avoid needing to pull in Substrate dependencies:
|
||||
//!
|
||||
//! - `sp_core::crypto::AccountId32` can be swapped with [`crate::utils::AccountId32`].
|
||||
//! - `sp_core::H256` is a re-export which subxt also provides as [`crate::config::substrate::H256`].
|
||||
//! - `sp_runtime::traits::BlakeTwo256` can be swapped with [`crate::config::substrate::BlakeTwo256`].
|
||||
//! - `sp_runtime::generic::Header` can be swapped with [`crate::config::substrate::SubstrateHeader`].
|
||||
//!
|
||||
//! Having a look at how those types are implemented can give some clues as to how to implement other custom types that
|
||||
//! you may need to use as part of your config.
|
||||
//!
|
||||
//! ## `Address`, `Signature`
|
||||
//!
|
||||
//! A Substrate runtime is typically constructed by using the [frame_support::construct_runtime](https://docs.rs/frame-support/latest/frame_support/macro.construct_runtime.html) macro.
|
||||
//! In this macro, we need to specify the type of an `UncheckedExtrinsic`. Most of the time, the `UncheckedExtrinsic` will be of the type
|
||||
//! `sp_runtime::generic::UncheckedExtrinsic<Address, RuntimeCall, Signature, SignedExtra>`.
|
||||
//! The generic parameters `Address` and `Signature` specified when declaring the `UncheckedExtrinsic` type
|
||||
//! are the types for `Address` and `Signature` we should use with our [crate::Config] implementation. This information can
|
||||
//! also be obtained from the metadata (see [`frame_metadata::v15::ExtrinsicMetadata`]). In case of Statemint (Polkadot Asset Hub)
|
||||
//! we see the following types being used in `UncheckedExtrinsic`:
|
||||
//!
|
||||
//! - Address: `sp_runtime::MultiAddress<Self::AccountId, ()>`
|
||||
//! - Signature: `sp_runtime::MultiSignature`
|
||||
//!
|
||||
//! As above, Subxt has its own versions of these types that can be used instead to avoid pulling in Substrate dependencies.
|
||||
//! Using the Subxt versions also makes interacting with generated code (which uses them in some places) a little nicer:
|
||||
//!
|
||||
//! - `sp_runtime::MultiAddress` can be swapped with [`crate::utils::MultiAddress`].
|
||||
//! - `sp_runtime::MultiSignature` can be swapped with [`crate::utils::MultiSignature`].
|
||||
//!
|
||||
//! ## ExtrinsicParams
|
||||
//!
|
||||
//! Chains each have a set of "transaction extensions" (formally called "signed extensions") configured. Transaction extensions provide
|
||||
//! a means to extend how transactions work. Each transaction extension can potentially encode some "extra" data which is sent along with a transaction, as well as some
|
||||
//! "additional" data which is included in the transaction signer payload, but not transmitted along with the transaction. On
|
||||
//! a node, transaction extensions can then perform additional checks on the submitted transactions to ensure their validity.
|
||||
//!
|
||||
//! The `ExtrinsicParams` config type expects to be given an implementation of the [`crate::config::ExtrinsicParams`] trait.
|
||||
//! Implementations of the [`crate::config::ExtrinsicParams`] trait are handed some parameters from Subxt itself, and can
|
||||
//! accept arbitrary other `Params` from users, and are then expected to provide this "extra" and "additional" data when asked
|
||||
//! via the required [`crate::config::ExtrinsicParamsEncoder`] impl.
|
||||
//!
|
||||
//! **In most cases, the default [crate::config::DefaultExtrinsicParams] type will work**: it understands the "standard"
|
||||
//! transaction extensions that are in use, and allows the user to provide things like a tip, and set the extrinsic mortality via
|
||||
//! [`crate::config::DefaultExtrinsicParamsBuilder`]. It will use the chain metadata to decide which transaction extensions to use
|
||||
//! and in which order. It will return an error if the chain uses a transaction extension which it doesn't know how to handle.
|
||||
//!
|
||||
//! If the chain uses novel transaction extensions (or if you just wish to provide a different interface for users to configure
|
||||
//! transactions), you can either:
|
||||
//!
|
||||
//! 1. Implement a new transaction extension and add it to the list.
|
||||
//! 2. Implement [`crate::config::DefaultExtrinsicParams`] from scratch.
|
||||
//!
|
||||
//! See below for examples of each.
|
||||
//!
|
||||
//! ### Finding out which transaction extensions a chain is using.
|
||||
//!
|
||||
//! In either case, you'll want to find out which transaction extensions a chain is using. This information can be obtained from
|
||||
//! the `SignedExtra` parameter of the `UncheckedExtrinsic` of your parachain, which will be a tuple of transaction extensions.
|
||||
//! It can also be obtained from the metadata (see [`frame_metadata::v15::SignedExtensionMetadata`]).
|
||||
//!
|
||||
//! For statemint, the transaction extensions look like
|
||||
//! [this](https://github.com/paritytech/cumulus/blob/d4bb2215bb28ee05159c4c7df1b3435177b5bf4e/parachains/runtimes/assets/asset-hub-polkadot/src/lib.rs#L786):
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! pub type SignedExtra = (
|
||||
//! frame_system::CheckNonZeroSender<Runtime>,
|
||||
//! frame_system::CheckSpecVersion<Runtime>,
|
||||
//! frame_system::CheckTxVersion<Runtime>,
|
||||
//! frame_system::CheckGenesis<Runtime>,
|
||||
//! frame_system::CheckEra<Runtime>,
|
||||
//! frame_system::CheckNonce<Runtime>,
|
||||
//! frame_system::CheckWeight<Runtime>,
|
||||
//! pallet_asset_tx_payment::ChargeAssetTxPayment<Runtime>,
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! Each element of the `SignedExtra` tuple implements [codec::Encode] and `sp_runtime::traits::SignedExtension`
|
||||
//! which has an associated type `AdditionalSigned` that also implements [codec::Encode]. Let's look at the underlying types
|
||||
//! for each tuple element. All zero-sized types have been replaced by `()` for simplicity.
|
||||
//!
|
||||
//! | tuple element | struct type | `AdditionalSigned` type |
|
||||
//! | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
||||
//! | [`frame_system::CheckNonZeroSender`](https://docs.rs/frame-system/latest/frame_system/struct.CheckNonZeroSender.html) | () | () |
|
||||
//! | [`frame_system::CheckSpecVersion`](https://docs.rs/frame-system/latest/frame_system/struct.CheckSpecVersion.html) | () | [u32] |
|
||||
//! | [`frame_system::CheckTxVersion`](https://docs.rs/frame-system/latest/frame_system/struct.CheckTxVersion.html) | () | [u32] |
|
||||
//! | [`frame_system::CheckGenesis`](https://docs.rs/frame-system/latest/frame_system/struct.CheckGenesis.html) | () | `Config::Hash` = `sp_core::H256` |
|
||||
//! | [`frame_system::CheckMortality`](https://docs.rs/frame-system/latest/frame_system/struct.CheckMortality.html) | `sp_runtime::generic::Era` | `Config::Hash` = `sp_core::H256` |
|
||||
//! | [`frame_system::CheckNonce`](https://docs.rs/frame-system/latest/frame_system/struct.CheckNonce.html) | `frame_system::pallet::Config::Index` = u32 | () |
|
||||
//! | [`frame_system::CheckWeight`](https://docs.rs/frame-system/latest/frame_system/struct.CheckWeight.html) | () | () |
|
||||
//! | [`frame_system::ChargeAssetTxPayment`](https://docs.rs/frame-system/latest/frame_system/struct.ChargeAssetTxPayment.html) | [pallet_asset_tx_payment::ChargeAssetTxPayment](https://docs.rs/pallet-asset-tx-payment/latest/pallet_asset_tx_payment/struct.ChargeAssetTxPayment.html) | () |
|
||||
//!
|
||||
//! All types in the `struct type` column make up the "extra" data that we're expected to provide. All types in the
|
||||
//! `AdditionalSigned` column make up the "additional" data that we're expected to provide. This information will be useful
|
||||
//! whether we want to implement [`crate::config::TransactionExtension`] for a transaction extension, or implement
|
||||
//! [`crate::config::ExtrinsicParams`] from scratch.
|
||||
//!
|
||||
//! As it happens, all of the transaction extensions in the table are either already exported in [`crate::config::transaction_extensions`],
|
||||
//! or they hand back no "additional" or "extra" data. In both of these cases, the default `ExtrinsicParams` configuration will
|
||||
//! work out of the box.
|
||||
//!
|
||||
//! ### Implementing and adding new transaction extensions to the config
|
||||
//!
|
||||
//! If you do need to implement a novel transaction extension, then you can implement [`crate::config::transaction_extensions::TransactionExtension`]
|
||||
//! on a custom type and place it into a new set of transaction extensions, like so:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str ! ("../../../examples/setup_config_transaction_extension.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Implementing [`crate::config::ExtrinsicParams`] from scratch
|
||||
//!
|
||||
//! Alternately, you are free to implement [`crate::config::ExtrinsicParams`] entirely from scratch if you know exactly what "extra" and
|
||||
//! "additional" data your node needs and would prefer to craft your own interface.
|
||||
//!
|
||||
//! Let's see what this looks like (this config won't work on any real node):
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str ! ("../../../examples/setup_config_custom.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Using a type from the metadata as a config parameter
|
||||
//!
|
||||
//! You can also use types that are generated from chain metadata as type parameters of the Config trait.
|
||||
//! Just make sure all trait bounds are satisfied. This can often be achieved by using custom derives with the subxt macro.
|
||||
//! For example, the AssetHub Parachain expects tips to include a `MultiLocation`, which is a type we can draw from the metadata.
|
||||
//!
|
||||
//! This example shows what using the `MultiLocation` struct as part of your config would look like in subxt:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str ! ("../../../examples/setup_config_assethub.rs")]
|
||||
//! ```
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This modules contains details on setting up Subxt:
|
||||
//!
|
||||
//! - [Codegen](codegen)
|
||||
//! - [Client](client)
|
||||
//!
|
||||
//! Alternately, [go back](super).
|
||||
|
||||
pub mod client;
|
||||
pub mod codegen;
|
||||
pub mod config;
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Blocks
|
||||
//!
|
||||
//! The [blocks API](crate::blocks::BlocksClient) in Subxt unifies many of the other interfaces, and
|
||||
//! allows you to:
|
||||
//!
|
||||
//! - Access information about specific blocks (see [`crate::blocks::BlocksClient::at()`] and
|
||||
//! [`crate::blocks::BlocksClient::at_latest()`]).
|
||||
//! - Subscribe to [all](crate::blocks::BlocksClient::subscribe_all()),
|
||||
//! [best](crate::blocks::BlocksClient::subscribe_best()) or
|
||||
//! [finalized](crate::blocks::BlocksClient::subscribe_finalized()) blocks as they are produced.
|
||||
//! **Prefer to subscribe to finalized blocks unless you know what you're doing.**
|
||||
//!
|
||||
//! In either case, you'll end up with [`crate::blocks::Block`]'s, from which you can access various
|
||||
//! information about the block, such a the [header](crate::blocks::Block::header()),
|
||||
//! [block number](crate::blocks::Block::number()) and [body (the extrinsics)](crate::blocks::Block::extrinsics()).
|
||||
//! [`crate::blocks::Block`]'s also provide shortcuts to other Subxt APIs that will operate at the
|
||||
//! given block:
|
||||
//!
|
||||
//! - [storage](crate::blocks::Block::storage()),
|
||||
//! - [events](crate::blocks::Block::events())
|
||||
//! - [runtime APIs](crate::blocks::Block::runtime_api())
|
||||
//!
|
||||
//! Aside from these links to other Subxt APIs, the main thing that we can do here is iterate over and
|
||||
//! decode the extrinsics in a block body.
|
||||
//!
|
||||
//! ## Decoding Extrinsics
|
||||
//!
|
||||
//! Given a block, you can [download the block body](crate::blocks::Block::extrinsics()) and
|
||||
//! [iterate over the extrinsics](crate::blocks::Extrinsics::iter) stored within it. The extrinsics yielded are of type
|
||||
//! [ExtrinsicDetails](crate::blocks::ExtrinsicDetails), which is just a blob of bytes that also stores which
|
||||
//! pallet and call in that pallet it belongs to. It also contains information about signed extensions that
|
||||
//! have been used for submitting this extrinsic.
|
||||
//!
|
||||
//! To use the extrinsic, you probably want to decode it into a concrete Rust type. These Rust types representing
|
||||
//! extrinsics from different pallets can be generated from metadata using the subxt macro or the CLI tool.
|
||||
//!
|
||||
//! When decoding the extrinsic into a static type you have two options:
|
||||
//!
|
||||
//! ### Statically decode the extrinsics into [the root extrinsic type](crate::blocks::ExtrinsicDetails::as_root_extrinsic())
|
||||
//!
|
||||
//! The root extrinsic type generated by subxt is a Rust enum with one variant for each pallet. Each of these
|
||||
//! variants has a field that is another enum whose variants cover all calls of the respective pallet.
|
||||
//! If the extrinsic bytes are valid and your metadata matches the chain's metadata, decoding the bytes of an extrinsic into
|
||||
//! this root extrinsic type should always succeed.
|
||||
//!
|
||||
//! This example shows how to subscribe to blocks and decode the extrinsics in each block into the root extrinsic type.
|
||||
//! Once we get hold of the [ExtrinsicDetails](crate::blocks::ExtrinsicDetails), we can decode it statically or dynamically.
|
||||
//! We can also access details about the extrinsic, including the associated events and transaction extensions.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/blocks_subscribing.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Statically decode the extrinsic into [a specific pallet call](crate::blocks::ExtrinsicDetails::as_extrinsic())
|
||||
//!
|
||||
//! This is useful if you are expecting a specific extrinsic to be part of some block. If the extrinsic you try to decode
|
||||
//! is a different extrinsic, an `Ok(None)` value is returned from [`as_extrinsic::<T>()`](crate::blocks::ExtrinsicDetails::as_extrinsic());
|
||||
//!
|
||||
//! If you are only interested in finding specific extrinsics in a block, you can also [iterate over all of them](crate::blocks::Extrinsics::find),
|
||||
//! get only [the first one](crate::blocks::Extrinsics::find_first), or [the last one](crate::blocks::Extrinsics::find_last).
|
||||
//!
|
||||
//! The following example monitors `TransferKeepAlive` extrinsics on the Polkadot network.
|
||||
//! We statically decode them and access the [tip](crate::blocks::ExtrinsicTransactionExtensions::tip()) and
|
||||
//! [account nonce](crate::blocks::ExtrinsicTransactionExtensions::nonce()) transaction extensions.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/block_decoding_static.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Dynamically decode the extrinsic
|
||||
//!
|
||||
//! Sometimes you might use subxt with metadata that is not known at compile time. In this case, you do not
|
||||
//! have access to a statically generated interface module that contains the relevant Rust types. You can
|
||||
//! [decode ExtrinsicDetails dynamically](crate::blocks::ExtrinsicDetails::decode_as_fields()), which gives
|
||||
//! you access to it's fields as a [scale value composite](scale_value::Composite). The following example
|
||||
//! looks for signed extrinsics on the Polkadot network and retrieves their pallet name, variant name, data
|
||||
//! fields and transaction extensions dynamically. Notice how we do not need to use code generation via the
|
||||
//! subxt macro. The only fixed component we provide is the [PolkadotConfig](crate::config::PolkadotConfig).
|
||||
//! Other than that it works in a chain-agnostic way:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/block_decoding_dynamic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ## Decoding transaction extensions
|
||||
//!
|
||||
//! Extrinsics can contain transaction extensions. The transaction extensions can be different across chains.
|
||||
//! The [Config](crate::Config) implementation for your chain defines which transaction extensions you expect.
|
||||
//! Once you get hold of the [ExtrinsicDetails](crate::blocks::ExtrinsicDetails) for an extrinsic you are interested in,
|
||||
//! you can try to [get its transaction extensions](crate::blocks::ExtrinsicDetails::transaction_extensions()).
|
||||
//! These are only available on V4 signed extrinsics or V5 general extrinsics. You can try to
|
||||
//! [find a specific transaction extension](crate::blocks::ExtrinsicTransactionExtensions::find), in the returned
|
||||
//! [transaction extensions](crate::blocks::ExtrinsicTransactionExtensions).
|
||||
//!
|
||||
//! Subxt also provides utility functions to get the [tip](crate::blocks::ExtrinsicTransactionExtensions::tip()) and
|
||||
//! the [account nonce](crate::blocks::ExtrinsicTransactionExtensions::nonce()) associated with an extrinsic, given
|
||||
//! its transaction extensions. If you prefer to do things dynamically you can get the data of the transaction extension
|
||||
//! as a [scale value](crate::blocks::ExtrinsicTransactionExtension::value()).
|
||||
//!
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Constants
|
||||
//!
|
||||
//! There are various constants stored in a node; the types and values of these are defined in a
|
||||
//! runtime, and can only change when the runtime is updated. Much like [`super::storage`], we can
|
||||
//! query these using Subxt by taking the following steps:
|
||||
//!
|
||||
//! 1. [Constructing a constant query](#constructing-a-query).
|
||||
//! 2. [Submitting the query to get back the associated value](#submitting-it).
|
||||
//!
|
||||
//! ## Constructing a constant query
|
||||
//!
|
||||
//! We can use the statically generated interface to build constant queries:
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! let constant_query = polkadot::constants().system().block_length();
|
||||
//! ```
|
||||
//!
|
||||
//! Alternately, we can dynamically construct a constant query. A dynamic query needs the return
|
||||
//! type to be specified, where we can use [`crate::dynamic::Value`] if unsure:
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! use subxt::dynamic::Value;
|
||||
//!
|
||||
//! let storage_query = subxt::dynamic::constant::<Value>("System", "BlockLength");
|
||||
//! ```
|
||||
//!
|
||||
//! ## Submitting it
|
||||
//!
|
||||
//! Call [`crate::constants::ConstantsClient::at()`] to return and decode the constant into the
|
||||
//! type given by the address, or [`crate::constants::ConstantsClient::bytes_at()`] to return the
|
||||
//! raw bytes for some constant.
|
||||
//!
|
||||
//! Constant values are pulled directly out of the node metadata which Subxt has
|
||||
//! already acquired, and so this function requires no network access and is available from a
|
||||
//! [`crate::OfflineClient`].
|
||||
//!
|
||||
//! Here's an example using a static query:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/constants_static.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! And here's one using a dynamic query:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/constants_dynamic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Custom Values
|
||||
//!
|
||||
//! Substrate-based chains can expose custom values in their metadata.
|
||||
//! Each of these values:
|
||||
//!
|
||||
//! - can be accessed by a unique __name__.
|
||||
//! - refers to a concrete __type__ stored in the metadata.
|
||||
//! - contains a scale encoded __value__ of that type.
|
||||
//!
|
||||
//! ## Getting a custom value
|
||||
//!
|
||||
//! First, you must construct an address to access a custom value. This can be either:
|
||||
//! - a raw [`str`] which assumes the return type to be the dynamic [`crate::dynamic::Value`] type,
|
||||
//! - created via [`dynamic`](crate::custom_values::dynamic) function whereby you set the return type
|
||||
//! that you want back,
|
||||
//! - created via statically generated addresses as part of the `#[subxt]` macro which define the return type.
|
||||
//!
|
||||
//! With an address, use [`at`](crate::custom_values::CustomValuesClient::at) to access and decode specific values, and
|
||||
//! [`bytes_at`](crate::custom_values::CustomValuesClient::bytes_at) to access the raw bytes.
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! Dynamically accessing a custom value using a [`str`] to select which one:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use subxt::{OnlineClient, PolkadotConfig, ext::scale_decode::DecodeAsType};
|
||||
//! use subxt::dynamic::Value;
|
||||
//!
|
||||
//! let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
//! let custom_value_client = api.custom_values();
|
||||
//! let foo: Value = custom_value_client.at("foo")?;
|
||||
//! ```
|
||||
//!
|
||||
//! Use the [`dynamic`](crate::custom_values::dynamic) function to select the return type:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use subxt::{OnlineClient, PolkadotConfig, ext::scale_decode::DecodeAsType};
|
||||
//!
|
||||
//! #[derive(Decode, DecodeAsType, Debug)]
|
||||
//! struct Foo {
|
||||
//! n: u8,
|
||||
//! b: bool,
|
||||
//! }
|
||||
//!
|
||||
//! let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
//! let custom_value_client = api.custom_values();
|
||||
//! let custom_value_addr = subxt::custom_values::dynamic::<Foo>("foo");
|
||||
//! let foo: Foo = custom_value_client.at(&custom_value_addr)?;
|
||||
//! ```
|
||||
//!
|
||||
//! Alternatively we also provide a statically generated api for custom values:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! #[subxt::subxt(runtime_metadata_path = "some_metadata.scale")]
|
||||
//! pub mod interface {}
|
||||
//!
|
||||
//! let static_address = interface::custom().foo();
|
||||
//!
|
||||
//! let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
//! let custom_value_client = api.custom_values();
|
||||
//!
|
||||
//! // Now the `at()` function already decodes the value into the Foo type:
|
||||
//! let foo = custom_value_client.at(&static_address)?;
|
||||
//! ```
|
||||
//!
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Events
|
||||
//!
|
||||
//! In the process of adding extrinsics to a block, they are executed. When extrinsics are executed,
|
||||
//! they normally produce events describing what's happening (at the very least, an event dictating whether
|
||||
//! the extrinsic has succeeded or failed). The node may also emit some events of its own as the block is
|
||||
//! processed.
|
||||
//!
|
||||
//! Events live in a single location in node storage which is overwritten at each block. Normal nodes tend to
|
||||
//! keep a snapshot of the state at a small number of previous blocks, so you can sometimes access
|
||||
//! older events by using [`crate::events::EventsClient::at()`] and providing an older block hash.
|
||||
//!
|
||||
//! When we submit transactions using Subxt, methods like [`crate::tx::TxProgress::wait_for_finalized_success()`]
|
||||
//! return [`crate::blocks::ExtrinsicEvents`], which can be used to iterate and inspect the events produced
|
||||
//! by that transaction being executed. We can also access _all_ of the events produced in a single block using one
|
||||
//! of these two interfaces:
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use subxt::client::OnlineClient;
|
||||
//! use subxt::config::PolkadotConfig;
|
||||
//!
|
||||
//! // Create client:
|
||||
//! let client = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
//!
|
||||
//! // Get events from the latest block (use .at() to specify a block hash):
|
||||
//! let events = client.blocks().at_latest().await?.events().await?;
|
||||
//! // We can use this shorthand too:
|
||||
//! let events = client.events().at_latest().await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Once we've loaded our events, we can iterate all events or search for specific events via
|
||||
//! methods like [`crate::events::Events::iter()`] and [`crate::events::Events::find()`]. See
|
||||
//! [`crate::events::Events`] and [`crate::events::EventDetails`] for more information.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! Here's an example which puts this all together:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/events.rs")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Light Client
|
||||
//!
|
||||
//! The light client based interface uses _Smoldot_ to connect to a _chain_, rather than an individual
|
||||
//! node. This means that you don't have to trust a specific node when interacting with some chain.
|
||||
//!
|
||||
//! This feature is currently unstable. Use the `unstable-light-client` feature flag to enable it.
|
||||
//! To use this in WASM environments, enable the `web` feature flag and disable the "native" one.
|
||||
//!
|
||||
//! To connect to a blockchain network, the Light Client requires a trusted sync state of the network,
|
||||
//! known as a _chain spec_. One way to obtain this is by making a `sync_state_genSyncSpec` RPC call to a
|
||||
//! trusted node belonging to the chain that you wish to interact with.
|
||||
//!
|
||||
//! Subxt exposes a utility method to obtain the chain spec: [`crate::utils::fetch_chainspec_from_rpc_node()`].
|
||||
//! Alternately, you can manually make an RPC call to `sync_state_genSyncSpec` like do (assuming a node running
|
||||
//! locally on port 9933):
|
||||
//!
|
||||
//! ```bash
|
||||
//! curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "sync_state_genSyncSpec", "params":[true]}' http://localhost:9933/ | jq .result > chain_spec.json
|
||||
//! ```
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! ### Basic Example
|
||||
//!
|
||||
//! This basic example uses some already-known chain specs to connect to a relay chain and parachain
|
||||
//! and stream information about their finalized blocks:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/light_client_basic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Connecting to a local node
|
||||
//!
|
||||
//! This example connects to a local chain and submits a transaction. To run this, you first need
|
||||
//! to have a local polkadot node running using the following command:
|
||||
//!
|
||||
//! ```text
|
||||
//! polkadot --dev --node-key 0000000000000000000000000000000000000000000000000000000000000001
|
||||
//! ```
|
||||
//!
|
||||
//! Then, the following code will download a chain spec from this local node, alter the bootnodes
|
||||
//! to point only to the local node, and then submit a transaction through it.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/light_client_local_node.rs")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This modules contains examples of using Subxt; follow the links for more:
|
||||
//!
|
||||
//! - [Transactions](transactions)
|
||||
//! - [Storage](storage)
|
||||
//! - [Events](events)
|
||||
//! - [Constants](constants)
|
||||
//! - [Blocks](blocks)
|
||||
//! - [Runtime APIs](runtime_apis)
|
||||
//! - [Unstable Light Client](light_client)
|
||||
//! - [Custom Values](custom_values)
|
||||
//! - [RPC calls](rpc)
|
||||
//!
|
||||
//! Alternately, [go back](super).
|
||||
|
||||
pub mod blocks;
|
||||
pub mod constants;
|
||||
pub mod custom_values;
|
||||
pub mod events;
|
||||
pub mod light_client;
|
||||
pub mod rpc;
|
||||
pub mod runtime_apis;
|
||||
pub mod storage;
|
||||
pub mod transactions;
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # RPC calls
|
||||
//!
|
||||
//! The RPC interface is provided by the [`pezkuwi_subxt_rpcs`] crate but re-exposed here. We have:
|
||||
//!
|
||||
//! - [`crate::backend::rpc::RpcClient`] and [`crate::backend::rpc::RpcClientT`]: the underlying type and trait
|
||||
//! which provides a basic RPC client.
|
||||
//! - [`crate::backend::legacy::rpc_methods`] and [`crate::backend::chain_head::rpc_methods`]: RPc methods that
|
||||
//! can be instantiated with an RPC client.
|
||||
//!
|
||||
//! See [`pezkuwi_subxt_rpcs`] or [`crate::ext::pezkuwi_subxt_rpcs`] for more.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Here's an example which calls some legacy JSON-RPC methods, and reuses the same connection to run a full Subxt client
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/rpc_legacy.rs")]
|
||||
//! ```
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Runtime API interface
|
||||
//!
|
||||
//! The Runtime API interface allows Subxt to call runtime APIs exposed by certain pallets in order
|
||||
//! to obtain information. Much like [`super::storage`] and [`super::transactions`], Making a runtime
|
||||
//! call to a node and getting the response back takes the following steps:
|
||||
//!
|
||||
//! 1. [Constructing a runtime call](#constructing-a-runtime-call)
|
||||
//! 2. [Submitting it to get back the response](#submitting-it)
|
||||
//!
|
||||
//! **Note:** Runtime APIs are only available when using V15 metadata, which is currently unstable.
|
||||
//! You'll need to use `subxt metadata --version unstable` command to download the unstable V15 metadata,
|
||||
//! and activate the `unstable-metadata` feature in Subxt for it to also use this metadata from a node. The
|
||||
//! metadata format is unstable because it may change and break compatibility with Subxt at any moment, so
|
||||
//! use at your own risk.
|
||||
//!
|
||||
//! ## Constructing a runtime call
|
||||
//!
|
||||
//! We can use the statically generated interface to build runtime calls:
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! let runtime_call = polkadot::apis().metadata().metadata_versions();
|
||||
//! ```
|
||||
//!
|
||||
//! Alternately, we can dynamically construct a runtime call. The input type can be a tuple or
|
||||
//! vec or valid types implementing [`scale_encode::EncodeAsType`], and the output can be anything
|
||||
//! implementing [`scale_decode::DecodeAsType`]:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use subxt::dynamic::Value;
|
||||
//!
|
||||
//! let runtime_call = subxt::dynamic::runtime_api_call::<(), Vec<u32>>(
|
||||
//! "Metadata",
|
||||
//! "metadata_versions",
|
||||
//! ()
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! All valid runtime calls implement [`crate::runtime_api::Payload`], a trait which
|
||||
//! describes how to encode the runtime call arguments and what return type to decode from the
|
||||
//! response.
|
||||
//!
|
||||
//! ## Submitting it
|
||||
//!
|
||||
//! Runtime calls can be handed to [`crate::runtime_api::RuntimeApi::call()`], which will submit
|
||||
//! them and hand back the associated response.
|
||||
//!
|
||||
//! ### Making a static Runtime API call
|
||||
//!
|
||||
//! The easiest way to make a runtime API call is to use the statically generated interface.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/runtime_apis_static.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Making a dynamic Runtime API call
|
||||
//!
|
||||
//! If you'd prefer to construct the call at runtime, you can do this using the
|
||||
//! [`crate::dynamic::runtime_api_call`] method.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/runtime_apis_dynamic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Making a raw call
|
||||
//!
|
||||
//! This is generally discouraged in favour of one of the above, but may be necessary (especially if
|
||||
//! the node you're talking to does not yet serve V15 metadata). Here, you must manually encode
|
||||
//! the argument bytes and manually provide a type for the response bytes to be decoded into.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/runtime_apis_raw.rs")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Storage
|
||||
//!
|
||||
//! A Substrate based chain can be seen as a key/value database which starts off at some initial
|
||||
//! state, and is modified by the extrinsics in each block. This database is referred to as the
|
||||
//! node storage. With Subxt, you can query this key/value storage with the following steps:
|
||||
//!
|
||||
//! 1. [Constructing a storage query](#constructing-a-storage-query).
|
||||
//! 2. [Submitting the query to get back the associated entry](#submitting-it).
|
||||
//! 3. [Fetching](#fetching-storage-entries) or [iterating](#iterating-storage-entries) over that
|
||||
//! entry to retrieve the value or values within it.
|
||||
//!
|
||||
//! ## Constructing a storage query
|
||||
//!
|
||||
//! We can use the statically generated interface to build storage queries:
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! let storage_query = polkadot::storage().system().account();
|
||||
//! ```
|
||||
//!
|
||||
//! Alternately, we can dynamically construct a storage query. A dynamic query needs the input
|
||||
//! and return value types to be specified, where we can use [`crate::dynamic::Value`] if unsure.
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! use subxt::dynamic::Value;
|
||||
//!
|
||||
//! let storage_query = subxt::dynamic::storage::<(Value,), Value>("System", "Account");
|
||||
//! ```
|
||||
//!
|
||||
//! ## Submitting it
|
||||
//!
|
||||
//! Storage queries can be handed to various functions in [`crate::storage::StorageClientAt`] in order to
|
||||
//! obtain the associated values (also referred to as storage entries) back.
|
||||
//!
|
||||
//! The core API here is [`crate::storage::StorageClientAt::entry()`], which takes a query and looks up the
|
||||
//! corresponding storage entry, from which you can then fetch or iterate over the values contained within.
|
||||
//! [`crate::storage::StorageClientAt::fetch()`] and [`crate::storage::StorageClientAt::iter()`] are shorthand
|
||||
//! for this.
|
||||
//!
|
||||
//! When you wish to manually query some entry, [`crate::storage::StorageClientAt::fetch_raw()`] exists to take
|
||||
//! in raw bytes pointing at some storage value, and return the value bytes if possible. [`crate::storage::StorageClientAt::storage_version()`]
|
||||
//! and [`crate::storage::StorageClientAt::runtime_wasm_code()`] use this to retrieve the version of some storage API
|
||||
//! and the current Runtime WASM blob respectively.
|
||||
//!
|
||||
//! ### Fetching storage entries
|
||||
//!
|
||||
//! The simplest way to access storage entries is to construct a query and then call either
|
||||
//! [`crate::storage::StorageClientAt::fetch()`]:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/storage_fetch.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! For completeness, below is an example using a dynamic query instead. Dynamic queries can define the types that
|
||||
//! they wish to accept inputs and decode the return value into ([`crate::dynamic::Value`] can be used here anywhere we
|
||||
//! are not sure of the specific types).
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/storage_fetch_dynamic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Iterating storage entries
|
||||
//!
|
||||
//! Many storage entries are maps of values; as well as fetching individual values, it's possible to
|
||||
//! iterate over all of the values stored at that location:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/storage_iterating.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! Here's the same logic but using dynamically constructed values instead:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/storage_iterating_dynamic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Transactions
|
||||
//!
|
||||
//! A transaction is an extrinsic that's signed (ie it originates from a given address). The purpose
|
||||
//! of extrinsics is to modify the node storage in a deterministic way, and so being able to submit
|
||||
//! transactions to a node is one of the core features of Subxt.
|
||||
//!
|
||||
//! > Note: the documentation tends to use the terms _extrinsic_ and _transaction_ interchangeably;
|
||||
//! > An extrinsic is some data that can be added to a block, and is either signed (a _transaction_)
|
||||
//! > or unsigned (an _inherent_). Subxt can construct either, but overwhelmingly you'll need to
|
||||
//! > sign the payload you'd like to submit.
|
||||
//!
|
||||
//! Submitting a transaction to a node consists of the following steps:
|
||||
//!
|
||||
//! 1. [Constructing a transaction payload to submit](#constructing-a-transaction-payload).
|
||||
//! 2. [Signing it](#signing-it).
|
||||
//! 3. [Submitting it (optionally with some additional parameters)](#submitting-it).
|
||||
//!
|
||||
//! We'll look at each of these steps in turn.
|
||||
//!
|
||||
//! ## Constructing a transaction payload
|
||||
//!
|
||||
//! We can use the statically generated interface to build transaction payloads:
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! let remark = "Hello there".as_bytes().to_vec();
|
||||
//! let tx_payload = polkadot::tx().system().remark(remark);
|
||||
//! ```
|
||||
//!
|
||||
//! > If you're not sure what types to import and use to build a given payload, you can use the
|
||||
//! > `subxt` CLI tool to generate the interface by using something like `subxt codegen | rustfmt >
|
||||
//! > interface.rs`, to see what types and things are available (or even just to use directly
|
||||
//! > instead of the [`#[subxt]`](crate::subxt) macro).
|
||||
//!
|
||||
//! Alternately, we can dynamically construct a transaction payload. This will not be type checked or
|
||||
//! validated until it's submitted:
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! use subxt::dynamic::Value;
|
||||
//!
|
||||
//! let tx_payload = subxt::dynamic::tx("System", "remark", vec![
|
||||
//! Value::from_bytes("Hello there")
|
||||
//! ]);
|
||||
//! ```
|
||||
//!
|
||||
//! The [`crate::dynamic::Value`] type is a dynamic type much like a `serde_json::Value` but instead
|
||||
//! represents any type of data that can be SCALE encoded or decoded. It can be serialized,
|
||||
//! deserialized and parsed from/to strings.
|
||||
//!
|
||||
//! A valid transaction payload is just something that implements the [`crate::tx::Payload`] trait;
|
||||
//! you can implement this trait on your own custom types if the built-in ones are not suitable for
|
||||
//! your needs.
|
||||
//!
|
||||
//! ## Signing it
|
||||
//!
|
||||
//! You'll normally need to sign an extrinsic to prove that it originated from an account that you
|
||||
//! control. To do this, you will typically first create a [`crate::tx::Signer`] instance, which tells
|
||||
//! Subxt who the extrinsic is from, and takes care of signing the relevant details to prove this.
|
||||
//!
|
||||
//! There are two main ways to create a compatible signer instance:
|
||||
//! 1. The `pezkuwi_subxt_signer` crate provides a WASM compatible implementation of [`crate::tx::Signer`]
|
||||
//! for chains which require sr25519 or ecdsa signatures (requires the `subxt` feature to be enabled).
|
||||
//! 2. Alternately, implement your own [`crate::tx::Signer`] instance by wrapping it in a new type pattern.
|
||||
//!
|
||||
//! Going for 1 leads to fewer dependencies being imported and WASM compatibility out of the box via
|
||||
//! the `web` feature flag. Going for 2 is useful if you're already using the Substrate dependencies or
|
||||
//! need additional signing algorithms that `pezkuwi_subxt_signer` doesn't support, and don't care about WASM
|
||||
//! compatibility.
|
||||
//!
|
||||
//! Because 2 is more complex and require more code, we'll focus on 1 here.
|
||||
//! For 2, see the example in `subxt/examples/substrate_compat_signer.rs` how
|
||||
//! you can integrate things like sp_core's signer in subxt.
|
||||
//!
|
||||
//! Let's go through how to create a signer using the `pezkuwi_subxt_signer` crate:
|
||||
//!
|
||||
//! ```rust,standalone_crate
|
||||
//! use subxt::config::PolkadotConfig;
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! use pezkuwi_subxt_signer::{SecretUri, sr25519};
|
||||
//!
|
||||
//! // Get hold of a `Signer` for a test account:
|
||||
//! let alice = sr25519::dev::alice();
|
||||
//!
|
||||
//! // Or generate a keypair, here from an SURI:
|
||||
//! let uri = SecretUri::from_str("vessel ladder alter error federal sibling chat ability sun glass valve picture/0/1///Password")
|
||||
//! .expect("valid URI");
|
||||
//! let keypair = sr25519::Keypair::from_uri(&uri)
|
||||
//! .expect("valid keypair");
|
||||
//!```
|
||||
//!
|
||||
//! After initializing the signer, let's also go through how to create a transaction and sign it:
|
||||
//!
|
||||
//! ```rust,no_run,standalone_crate
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use subxt::client::OnlineClient;
|
||||
//! use subxt::config::PolkadotConfig;
|
||||
//! use subxt::dynamic::Value;
|
||||
//!
|
||||
//! // Create client:
|
||||
//! let client = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
//!
|
||||
//! // Create a dummy tx payload to sign:
|
||||
//! let payload = subxt::dynamic::tx("System", "remark", vec![
|
||||
//! Value::from_bytes("Hello there")
|
||||
//! ]);
|
||||
//!
|
||||
//! // Construct the tx but don't sign it. The account nonce here defaults to 0.
|
||||
//! // You can use `create_partial` to fetch the correct nonce.
|
||||
//! let mut partial_tx = client.tx().create_partial_offline(
|
||||
//! &payload,
|
||||
//! Default::default()
|
||||
//! )?;
|
||||
//!
|
||||
//! // Fetch the payload that needs to be signed:
|
||||
//! let signer_payload = partial_tx.signer_payload();
|
||||
//!
|
||||
//! // ... At this point, we can hand off the `signer_payload` to be signed externally.
|
||||
//! // Ultimately we need to be given back a `signature` (or really, anything
|
||||
//! // that can be SCALE encoded) and an `address`:
|
||||
//! let signature;
|
||||
//! let account_id;
|
||||
//! # use subxt::tx::Signer;
|
||||
//! # let signer = pezkuwi_subxt_signer::sr25519::dev::alice();
|
||||
//! # signature = signer.sign(&signer_payload).into();
|
||||
//! # account_id = signer.public_key().to_account_id();
|
||||
//!
|
||||
//! // Now we can build an tx, which one can call `submit` or `submit_and_watch`
|
||||
//! // on to submit to a node and optionally watch the status.
|
||||
//! let tx = partial_tx.sign_with_account_and_signature(
|
||||
//! &account_id,
|
||||
//! &signature
|
||||
//! );
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Submitting it
|
||||
//!
|
||||
//! Once we have signed the transaction, we need to submit it.
|
||||
//!
|
||||
//! ### The high level API
|
||||
//!
|
||||
//! The highest level approach to doing this is to call
|
||||
//! [`crate::tx::TxClient::sign_and_submit_then_watch_default`]. This hands back a
|
||||
//! [`crate::tx::TxProgress`] struct which will monitor the transaction status. We can then call
|
||||
//! [`crate::tx::TxProgress::wait_for_finalized_success()`] to wait for this transaction to make it
|
||||
//! into a finalized block, check for an `ExtrinsicSuccess` event, and then hand back the events for
|
||||
//! inspection. This looks like:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/tx_basic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! ### Providing transaction parameters
|
||||
//!
|
||||
//! If you'd like to provide parameters (such as mortality) to the transaction, you can use
|
||||
//! [`crate::tx::TxClient::sign_and_submit_then_watch`] instead:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../../../examples/tx_with_params.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! 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
|
||||
//! the transaction, you can monitor them as they are emitted and react however you choose:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![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.
|
||||
//!
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module provides two clients that can be used to work with
|
||||
//! transactions, storage and events. The [`OfflineClient`] works
|
||||
//! entirely offline and can be passed to any function that doesn't
|
||||
//! require network access. The [`OnlineClient`] requires network
|
||||
//! access.
|
||||
|
||||
mod offline_client;
|
||||
mod online_client;
|
||||
|
||||
pub use offline_client::{OfflineClient, OfflineClientT};
|
||||
pub use online_client::{
|
||||
ClientRuntimeUpdater, OnlineClient, OnlineClientT, RuntimeUpdaterStream, Update,
|
||||
};
|
||||
pub use pezkuwi_subxt_core::client::{ClientState, RuntimeVersion};
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::custom_values::CustomValuesClient;
|
||||
use crate::{
|
||||
Metadata,
|
||||
blocks::BlocksClient,
|
||||
config::{Config, HashFor},
|
||||
constants::ConstantsClient,
|
||||
events::EventsClient,
|
||||
runtime_api::RuntimeApiClient,
|
||||
storage::StorageClient,
|
||||
tx::TxClient,
|
||||
view_functions::ViewFunctionsClient,
|
||||
};
|
||||
|
||||
use derive_where::derive_where;
|
||||
use std::sync::Arc;
|
||||
use pezkuwi_subxt_core::client::{ClientState, RuntimeVersion};
|
||||
|
||||
/// A trait representing a client that can perform
|
||||
/// offline-only actions.
|
||||
pub trait OfflineClientT<T: Config>: Clone + Send + Sync + 'static {
|
||||
/// Return the provided [`Metadata`].
|
||||
fn metadata(&self) -> Metadata;
|
||||
|
||||
/// Return the provided genesis hash.
|
||||
fn genesis_hash(&self) -> HashFor<T>;
|
||||
|
||||
/// Return the provided [`RuntimeVersion`].
|
||||
fn runtime_version(&self) -> RuntimeVersion;
|
||||
|
||||
/// Return the hasher used on the chain.
|
||||
fn hasher(&self) -> T::Hasher;
|
||||
|
||||
/// Return the [pezkuwi_subxt_core::client::ClientState] (metadata, runtime version and genesis hash).
|
||||
fn client_state(&self) -> ClientState<T> {
|
||||
ClientState {
|
||||
genesis_hash: self.genesis_hash(),
|
||||
runtime_version: self.runtime_version(),
|
||||
metadata: self.metadata(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Work with transactions.
|
||||
fn tx(&self) -> TxClient<T, Self> {
|
||||
TxClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Work with events.
|
||||
fn events(&self) -> EventsClient<T, Self> {
|
||||
EventsClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Work with storage.
|
||||
fn storage(&self) -> StorageClient<T, Self> {
|
||||
StorageClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Access constants.
|
||||
fn constants(&self) -> ConstantsClient<T, Self> {
|
||||
ConstantsClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Work with blocks.
|
||||
fn blocks(&self) -> BlocksClient<T, Self> {
|
||||
BlocksClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Work with runtime APIs.
|
||||
fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
|
||||
RuntimeApiClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Work with View Functions.
|
||||
fn view_functions(&self) -> ViewFunctionsClient<T, Self> {
|
||||
ViewFunctionsClient::new(self.clone())
|
||||
}
|
||||
|
||||
/// Work this custom types.
|
||||
fn custom_values(&self) -> CustomValuesClient<T, Self> {
|
||||
CustomValuesClient::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A client that is capable of performing offline-only operations.
|
||||
/// Can be constructed as long as you can populate the required fields.
|
||||
#[derive_where(Debug, Clone)]
|
||||
pub struct OfflineClient<T: Config> {
|
||||
inner: Arc<ClientState<T>>,
|
||||
hasher: T::Hasher,
|
||||
}
|
||||
|
||||
impl<T: Config> OfflineClient<T> {
|
||||
/// Construct a new [`OfflineClient`], providing
|
||||
/// the necessary runtime and compile-time arguments.
|
||||
pub fn new(
|
||||
genesis_hash: HashFor<T>,
|
||||
runtime_version: RuntimeVersion,
|
||||
metadata: impl Into<Metadata>,
|
||||
) -> OfflineClient<T> {
|
||||
let metadata = metadata.into();
|
||||
let hasher = <T::Hasher as pezkuwi_subxt_core::config::Hasher>::new(&metadata);
|
||||
|
||||
OfflineClient {
|
||||
hasher,
|
||||
inner: Arc::new(ClientState {
|
||||
genesis_hash,
|
||||
runtime_version,
|
||||
metadata,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the genesis hash.
|
||||
pub fn genesis_hash(&self) -> HashFor<T> {
|
||||
self.inner.genesis_hash
|
||||
}
|
||||
|
||||
/// Return the runtime version.
|
||||
pub fn runtime_version(&self) -> RuntimeVersion {
|
||||
self.inner.runtime_version
|
||||
}
|
||||
|
||||
/// Return the [`Metadata`] used in this client.
|
||||
pub fn metadata(&self) -> Metadata {
|
||||
self.inner.metadata.clone()
|
||||
}
|
||||
|
||||
/// Return the hasher used for the chain.
|
||||
pub fn hasher(&self) -> T::Hasher {
|
||||
self.hasher
|
||||
}
|
||||
|
||||
// Just a copy of the most important trait methods so that people
|
||||
// don't need to import the trait for most things:
|
||||
|
||||
/// Work with transactions.
|
||||
pub fn tx(&self) -> TxClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::tx(self)
|
||||
}
|
||||
|
||||
/// Work with events.
|
||||
pub fn events(&self) -> EventsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::events(self)
|
||||
}
|
||||
|
||||
/// Work with storage.
|
||||
pub fn storage(&self) -> StorageClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::storage(self)
|
||||
}
|
||||
|
||||
/// Access constants.
|
||||
pub fn constants(&self) -> ConstantsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::constants(self)
|
||||
}
|
||||
|
||||
/// Work with blocks.
|
||||
pub fn blocks(&self) -> BlocksClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::blocks(self)
|
||||
}
|
||||
|
||||
/// Work with runtime APIs.
|
||||
pub fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::runtime_api(self)
|
||||
}
|
||||
|
||||
/// Work with View Functions.
|
||||
pub fn view_functions(&self) -> ViewFunctionsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::view_functions(self)
|
||||
}
|
||||
|
||||
/// Access custom types
|
||||
pub fn custom_values(&self) -> CustomValuesClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::custom_values(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OfflineClientT<T> for OfflineClient<T> {
|
||||
fn genesis_hash(&self) -> HashFor<T> {
|
||||
self.genesis_hash()
|
||||
}
|
||||
fn runtime_version(&self) -> RuntimeVersion {
|
||||
self.runtime_version()
|
||||
}
|
||||
fn metadata(&self) -> Metadata {
|
||||
self.metadata()
|
||||
}
|
||||
fn hasher(&self) -> T::Hasher {
|
||||
self.hasher()
|
||||
}
|
||||
}
|
||||
|
||||
// For ergonomics; cloning a client is deliberately fairly cheap (via Arc),
|
||||
// so this allows users to pass references to a client rather than explicitly
|
||||
// cloning. This is partly for consistency with OnlineClient, which can be
|
||||
// easily converted into an OfflineClient for ergonomics.
|
||||
impl<'a, T: Config> From<&'a OfflineClient<T>> for OfflineClient<T> {
|
||||
fn from(c: &'a OfflineClient<T>) -> Self {
|
||||
c.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::{OfflineClient, OfflineClientT};
|
||||
use crate::custom_values::CustomValuesClient;
|
||||
use crate::{
|
||||
Metadata,
|
||||
backend::{Backend, BackendExt, StreamOfResults, legacy::LegacyBackend, rpc::RpcClient},
|
||||
blocks::{BlockRef, BlocksClient},
|
||||
config::{Config, HashFor},
|
||||
constants::ConstantsClient,
|
||||
error::{BackendError, OnlineClientError, RuntimeUpdateeApplyError, RuntimeUpdaterError},
|
||||
events::EventsClient,
|
||||
runtime_api::RuntimeApiClient,
|
||||
storage::StorageClient,
|
||||
tx::TxClient,
|
||||
view_functions::ViewFunctionsClient,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use futures::TryFutureExt;
|
||||
use futures::future;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use pezkuwi_subxt_core::client::{ClientState, RuntimeVersion};
|
||||
|
||||
/// A trait representing a client that can perform
|
||||
/// online actions.
|
||||
pub trait OnlineClientT<T: Config>: OfflineClientT<T> {
|
||||
/// Return a backend that can be used to communicate with a node.
|
||||
fn backend(&self) -> &dyn Backend<T>;
|
||||
}
|
||||
|
||||
/// A client that can be used to perform API calls (that is, either those
|
||||
/// requiring an [`OfflineClientT`] or those requiring an [`OnlineClientT`]).
|
||||
#[derive_where(Clone)]
|
||||
pub struct OnlineClient<T: Config> {
|
||||
inner: Arc<RwLock<Inner<T>>>,
|
||||
backend: Arc<dyn Backend<T>>,
|
||||
}
|
||||
|
||||
#[derive_where(Debug)]
|
||||
struct Inner<T: Config> {
|
||||
genesis_hash: HashFor<T>,
|
||||
runtime_version: RuntimeVersion,
|
||||
metadata: Metadata,
|
||||
hasher: T::Hasher,
|
||||
}
|
||||
|
||||
impl<T: Config> std::fmt::Debug for OnlineClient<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Client")
|
||||
.field("rpc", &"RpcClient")
|
||||
.field("inner", &self.inner)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// The default constructors assume Jsonrpsee.
|
||||
#[cfg(feature = "jsonrpsee")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "jsonrpsee")))]
|
||||
impl<T: Config> OnlineClient<T> {
|
||||
/// Construct a new [`OnlineClient`] using default settings which
|
||||
/// point to a locally running node on `ws://127.0.0.1:9944`.
|
||||
pub async fn new() -> Result<OnlineClient<T>, OnlineClientError> {
|
||||
let url = "ws://127.0.0.1:9944";
|
||||
OnlineClient::from_url(url).await
|
||||
}
|
||||
|
||||
/// Construct a new [`OnlineClient`], providing a URL to connect to.
|
||||
pub async fn from_url(url: impl AsRef<str>) -> Result<OnlineClient<T>, OnlineClientError> {
|
||||
pezkuwi_subxt_rpcs::utils::validate_url_is_secure(url.as_ref())?;
|
||||
OnlineClient::from_insecure_url(url).await
|
||||
}
|
||||
|
||||
/// Construct a new [`OnlineClient`], providing a URL to connect to.
|
||||
///
|
||||
/// Allows insecure URLs without SSL encryption, e.g. (http:// and ws:// URLs).
|
||||
pub async fn from_insecure_url(
|
||||
url: impl AsRef<str>,
|
||||
) -> Result<OnlineClient<T>, OnlineClientError> {
|
||||
let client = RpcClient::from_insecure_url(url).await?;
|
||||
let backend = LegacyBackend::builder().build(client);
|
||||
OnlineClient::from_backend(Arc::new(backend)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OnlineClient<T> {
|
||||
/// Construct a new [`OnlineClient`] by providing an [`RpcClient`] to drive the connection.
|
||||
/// This will use the current default [`Backend`], which may change in future releases.
|
||||
pub async fn from_rpc_client(
|
||||
rpc_client: impl Into<RpcClient>,
|
||||
) -> Result<OnlineClient<T>, OnlineClientError> {
|
||||
let rpc_client = rpc_client.into();
|
||||
let backend = Arc::new(LegacyBackend::builder().build(rpc_client));
|
||||
OnlineClient::from_backend(backend).await
|
||||
}
|
||||
|
||||
/// Construct a new [`OnlineClient`] by providing an RPC client along with the other
|
||||
/// necessary details. This will use the current default [`Backend`], which may change
|
||||
/// in future releases.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This is considered the most primitive and also error prone way to
|
||||
/// instantiate a client; the genesis hash, metadata and runtime version provided will
|
||||
/// entirely determine which node and blocks this client will be able to interact with,
|
||||
/// and whether it will be able to successfully do things like submit transactions.
|
||||
///
|
||||
/// If you're unsure what you're doing, prefer one of the alternate methods to instantiate
|
||||
/// a client.
|
||||
pub fn from_rpc_client_with(
|
||||
genesis_hash: HashFor<T>,
|
||||
runtime_version: RuntimeVersion,
|
||||
metadata: impl Into<Metadata>,
|
||||
rpc_client: impl Into<RpcClient>,
|
||||
) -> Result<OnlineClient<T>, OnlineClientError> {
|
||||
let rpc_client = rpc_client.into();
|
||||
let backend = Arc::new(LegacyBackend::builder().build(rpc_client));
|
||||
OnlineClient::from_backend_with(genesis_hash, runtime_version, metadata, backend)
|
||||
}
|
||||
|
||||
/// Construct a new [`OnlineClient`] by providing an underlying [`Backend`]
|
||||
/// implementation to power it. Other details will be obtained from the chain.
|
||||
pub async fn from_backend<B: Backend<T>>(
|
||||
backend: Arc<B>,
|
||||
) -> Result<OnlineClient<T>, OnlineClientError> {
|
||||
let latest_block = backend
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(OnlineClientError::CannotGetLatestFinalizedBlock)?;
|
||||
|
||||
let (genesis_hash, runtime_version, metadata) = future::join3(
|
||||
backend
|
||||
.genesis_hash()
|
||||
.map_err(OnlineClientError::CannotGetGenesisHash),
|
||||
backend
|
||||
.current_runtime_version()
|
||||
.map_err(OnlineClientError::CannotGetCurrentRuntimeVersion),
|
||||
OnlineClient::fetch_metadata(&*backend, latest_block.hash())
|
||||
.map_err(OnlineClientError::CannotFetchMetadata),
|
||||
)
|
||||
.await;
|
||||
|
||||
OnlineClient::from_backend_with(genesis_hash?, runtime_version?, metadata?, backend)
|
||||
}
|
||||
|
||||
/// Construct a new [`OnlineClient`] by providing all of the underlying details needed
|
||||
/// to make it work.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This is considered the most primitive and also error prone way to
|
||||
/// instantiate a client; the genesis hash, metadata and runtime version provided will
|
||||
/// entirely determine which node and blocks this client will be able to interact with,
|
||||
/// and whether it will be able to successfully do things like submit transactions.
|
||||
///
|
||||
/// If you're unsure what you're doing, prefer one of the alternate methods to instantiate
|
||||
/// a client.
|
||||
pub fn from_backend_with<B: Backend<T>>(
|
||||
genesis_hash: HashFor<T>,
|
||||
runtime_version: RuntimeVersion,
|
||||
metadata: impl Into<Metadata>,
|
||||
backend: Arc<B>,
|
||||
) -> Result<OnlineClient<T>, OnlineClientError> {
|
||||
use pezkuwi_subxt_core::config::Hasher;
|
||||
|
||||
let metadata = metadata.into();
|
||||
let hasher = T::Hasher::new(&metadata);
|
||||
|
||||
Ok(OnlineClient {
|
||||
inner: Arc::new(RwLock::new(Inner {
|
||||
genesis_hash,
|
||||
runtime_version,
|
||||
metadata,
|
||||
hasher,
|
||||
})),
|
||||
backend,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch the metadata from substrate using the runtime API.
|
||||
async fn fetch_metadata(
|
||||
backend: &dyn Backend<T>,
|
||||
block_hash: HashFor<T>,
|
||||
) -> Result<Metadata, BackendError> {
|
||||
#[cfg(feature = "unstable-metadata")]
|
||||
{
|
||||
/// The unstable metadata version number.
|
||||
const UNSTABLE_METADATA_VERSION: u32 = u32::MAX;
|
||||
|
||||
// Try to fetch the latest unstable metadata, if that fails fall back to
|
||||
// fetching the latest stable metadata.
|
||||
match backend
|
||||
.metadata_at_version(UNSTABLE_METADATA_VERSION, block_hash)
|
||||
.await
|
||||
{
|
||||
Ok(bytes) => Ok(bytes),
|
||||
Err(_) => OnlineClient::fetch_latest_stable_metadata(backend, block_hash).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unstable-metadata"))]
|
||||
OnlineClient::fetch_latest_stable_metadata(backend, block_hash).await
|
||||
}
|
||||
|
||||
/// Fetch the latest stable metadata from the node.
|
||||
async fn fetch_latest_stable_metadata(
|
||||
backend: &dyn Backend<T>,
|
||||
block_hash: HashFor<T>,
|
||||
) -> Result<Metadata, BackendError> {
|
||||
// The metadata versions we support in Subxt, from newest to oldest.
|
||||
use pezkuwi_subxt_metadata::SUPPORTED_METADATA_VERSIONS;
|
||||
|
||||
// Try to fetch each version that we support in order from newest to oldest.
|
||||
for version in SUPPORTED_METADATA_VERSIONS {
|
||||
if let Ok(bytes) = backend.metadata_at_version(version, block_hash).await {
|
||||
return Ok(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// If that fails, fetch the metadata V14 using the old API.
|
||||
backend.legacy_metadata(block_hash).await
|
||||
}
|
||||
|
||||
/// Create an object which can be used to keep the runtime up to date
|
||||
/// in a separate thread.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// use subxt::{ OnlineClient, PolkadotConfig };
|
||||
///
|
||||
/// let client = OnlineClient::<PolkadotConfig>::new().await.unwrap();
|
||||
///
|
||||
/// // high level API.
|
||||
///
|
||||
/// let update_task = client.updater();
|
||||
/// tokio::spawn(async move {
|
||||
/// update_task.perform_runtime_updates().await;
|
||||
/// });
|
||||
///
|
||||
///
|
||||
/// // low level API.
|
||||
///
|
||||
/// let updater = client.updater();
|
||||
/// tokio::spawn(async move {
|
||||
/// let mut update_stream = updater.runtime_updates().await.unwrap();
|
||||
///
|
||||
/// while let Ok(update) = update_stream.next().await {
|
||||
/// let version = update.runtime_version().spec_version;
|
||||
///
|
||||
/// match updater.apply_update(update) {
|
||||
/// Ok(()) => {
|
||||
/// println!("Upgrade to version: {} successful", version)
|
||||
/// }
|
||||
/// Err(e) => {
|
||||
/// println!("Upgrade to version {} failed {:?}", version, e);
|
||||
/// }
|
||||
/// };
|
||||
/// }
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn updater(&self) -> ClientRuntimeUpdater<T> {
|
||||
ClientRuntimeUpdater(self.clone())
|
||||
}
|
||||
|
||||
/// Return the hasher configured for hashing blocks and extrinsics.
|
||||
pub fn hasher(&self) -> T::Hasher {
|
||||
self.inner.read().expect("shouldn't be poisoned").hasher
|
||||
}
|
||||
|
||||
/// Return the [`Metadata`] used in this client.
|
||||
pub fn metadata(&self) -> Metadata {
|
||||
let inner = self.inner.read().expect("shouldn't be poisoned");
|
||||
inner.metadata.clone()
|
||||
}
|
||||
|
||||
/// Change the [`Metadata`] used in this client.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// Setting custom metadata may leave Subxt unable to work with certain blocks,
|
||||
/// subscribe to latest blocks or submit valid transactions.
|
||||
pub fn set_metadata(&self, metadata: impl Into<Metadata>) {
|
||||
let mut inner = self.inner.write().expect("shouldn't be poisoned");
|
||||
inner.metadata = metadata.into();
|
||||
}
|
||||
|
||||
/// Return the genesis hash.
|
||||
pub fn genesis_hash(&self) -> HashFor<T> {
|
||||
let inner = self.inner.read().expect("shouldn't be poisoned");
|
||||
inner.genesis_hash
|
||||
}
|
||||
|
||||
/// Change the genesis hash used in this client.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// Setting a custom genesis hash may leave Subxt unable to
|
||||
/// submit valid transactions.
|
||||
pub fn set_genesis_hash(&self, genesis_hash: HashFor<T>) {
|
||||
let mut inner = self.inner.write().expect("shouldn't be poisoned");
|
||||
inner.genesis_hash = genesis_hash;
|
||||
}
|
||||
|
||||
/// Return the runtime version.
|
||||
pub fn runtime_version(&self) -> RuntimeVersion {
|
||||
let inner = self.inner.read().expect("shouldn't be poisoned");
|
||||
inner.runtime_version
|
||||
}
|
||||
|
||||
/// Change the [`RuntimeVersion`] used in this client.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// Setting a custom runtime version may leave Subxt unable to
|
||||
/// submit valid transactions.
|
||||
pub fn set_runtime_version(&self, runtime_version: RuntimeVersion) {
|
||||
let mut inner = self.inner.write().expect("shouldn't be poisoned");
|
||||
inner.runtime_version = runtime_version;
|
||||
}
|
||||
|
||||
/// Return an RPC client to make raw requests with.
|
||||
pub fn backend(&self) -> &dyn Backend<T> {
|
||||
&*self.backend
|
||||
}
|
||||
|
||||
/// Return an offline client with the same configuration as this.
|
||||
pub fn offline(&self) -> OfflineClient<T> {
|
||||
let inner = self.inner.read().expect("shouldn't be poisoned");
|
||||
OfflineClient::new(
|
||||
inner.genesis_hash,
|
||||
inner.runtime_version,
|
||||
inner.metadata.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
// Just a copy of the most important trait methods so that people
|
||||
// don't need to import the trait for most things:
|
||||
|
||||
/// Work with transactions.
|
||||
pub fn tx(&self) -> TxClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::tx(self)
|
||||
}
|
||||
|
||||
/// Work with events.
|
||||
pub fn events(&self) -> EventsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::events(self)
|
||||
}
|
||||
|
||||
/// Work with storage.
|
||||
pub fn storage(&self) -> StorageClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::storage(self)
|
||||
}
|
||||
|
||||
/// Access constants.
|
||||
pub fn constants(&self) -> ConstantsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::constants(self)
|
||||
}
|
||||
|
||||
/// Work with blocks.
|
||||
pub fn blocks(&self) -> BlocksClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::blocks(self)
|
||||
}
|
||||
|
||||
/// Work with runtime API.
|
||||
pub fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::runtime_api(self)
|
||||
}
|
||||
|
||||
/// Work with View Functions.
|
||||
pub fn view_functions(&self) -> ViewFunctionsClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::view_functions(self)
|
||||
}
|
||||
|
||||
/// Access custom types.
|
||||
pub fn custom_values(&self) -> CustomValuesClient<T, Self> {
|
||||
<Self as OfflineClientT<T>>::custom_values(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OfflineClientT<T> for OnlineClient<T> {
|
||||
fn metadata(&self) -> Metadata {
|
||||
self.metadata()
|
||||
}
|
||||
fn genesis_hash(&self) -> HashFor<T> {
|
||||
self.genesis_hash()
|
||||
}
|
||||
fn runtime_version(&self) -> RuntimeVersion {
|
||||
self.runtime_version()
|
||||
}
|
||||
fn hasher(&self) -> T::Hasher {
|
||||
self.hasher()
|
||||
}
|
||||
// This is provided by default, but we can optimise here and only lock once:
|
||||
fn client_state(&self) -> ClientState<T> {
|
||||
let inner = self.inner.read().expect("shouldn't be poisoned");
|
||||
ClientState {
|
||||
genesis_hash: inner.genesis_hash,
|
||||
runtime_version: inner.runtime_version,
|
||||
metadata: inner.metadata.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OnlineClientT<T> for OnlineClient<T> {
|
||||
fn backend(&self) -> &dyn Backend<T> {
|
||||
&*self.backend
|
||||
}
|
||||
}
|
||||
|
||||
/// Client wrapper for performing runtime updates. See [`OnlineClient::updater()`]
|
||||
/// for example usage.
|
||||
pub struct ClientRuntimeUpdater<T: Config>(OnlineClient<T>);
|
||||
|
||||
impl<T: Config> ClientRuntimeUpdater<T> {
|
||||
fn is_runtime_version_different(&self, new: &RuntimeVersion) -> bool {
|
||||
let curr = self.0.inner.read().expect("shouldn't be poisoned");
|
||||
&curr.runtime_version != new
|
||||
}
|
||||
|
||||
fn do_update(&self, update: Update) {
|
||||
let mut writable = self.0.inner.write().expect("shouldn't be poisoned");
|
||||
writable.metadata = update.metadata;
|
||||
writable.runtime_version = update.runtime_version;
|
||||
}
|
||||
|
||||
/// Tries to apply a new update.
|
||||
pub fn apply_update(&self, update: Update) -> Result<(), RuntimeUpdateeApplyError> {
|
||||
if !self.is_runtime_version_different(&update.runtime_version) {
|
||||
return Err(RuntimeUpdateeApplyError::SameVersion);
|
||||
}
|
||||
|
||||
self.do_update(update);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs runtime updates indefinitely unless encountering an error.
|
||||
///
|
||||
/// *Note:* This will run indefinitely until it errors, so the typical usage
|
||||
/// would be to run it in a separate background task.
|
||||
pub async fn perform_runtime_updates(&self) -> Result<(), RuntimeUpdaterError> {
|
||||
// Obtain an update subscription to further detect changes in the runtime version of the node.
|
||||
let mut runtime_version_stream = self.runtime_updates().await?;
|
||||
|
||||
loop {
|
||||
let update = runtime_version_stream.next().await?;
|
||||
|
||||
// This only fails if received the runtime version is the same the current runtime version
|
||||
// which might occur because that runtime subscriptions in substrate sends out the initial
|
||||
// value when they created and not only when runtime upgrades occurs.
|
||||
// Thus, fine to ignore here as it strictly speaking isn't really an error
|
||||
let _ = self.apply_update(update);
|
||||
}
|
||||
}
|
||||
|
||||
/// Low-level API to get runtime updates as a stream but it's doesn't check if the
|
||||
/// runtime version is newer or updates the runtime.
|
||||
///
|
||||
/// 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>, RuntimeUpdaterError> {
|
||||
let stream = self
|
||||
.0
|
||||
.backend()
|
||||
.stream_runtime_version()
|
||||
.await
|
||||
.map_err(RuntimeUpdaterError::CannotStreamRuntimeVersion)?;
|
||||
|
||||
Ok(RuntimeUpdaterStream {
|
||||
stream,
|
||||
client: self.0.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream to perform runtime upgrades.
|
||||
pub struct RuntimeUpdaterStream<T: Config> {
|
||||
stream: StreamOfResults<RuntimeVersion>,
|
||||
client: OnlineClient<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> RuntimeUpdaterStream<T> {
|
||||
/// Wait for the next runtime update.
|
||||
pub async fn next(&mut self) -> Result<Update, RuntimeUpdaterError> {
|
||||
let runtime_version = self
|
||||
.stream
|
||||
.next()
|
||||
.await
|
||||
.ok_or(RuntimeUpdaterError::UnexpectedEndOfUpdateStream)?
|
||||
.map_err(RuntimeUpdaterError::CannotGetNextRuntimeVersion)?;
|
||||
|
||||
let at = wait_runtime_upgrade_in_finalized_block(&self.client, &runtime_version).await?;
|
||||
|
||||
let metadata = OnlineClient::fetch_metadata(self.client.backend(), at.hash())
|
||||
.await
|
||||
.map_err(RuntimeUpdaterError::CannotFetchNewMetadata)?;
|
||||
|
||||
Ok(Update {
|
||||
metadata,
|
||||
runtime_version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state when a runtime upgrade occurred.
|
||||
pub struct Update {
|
||||
runtime_version: RuntimeVersion,
|
||||
metadata: Metadata,
|
||||
}
|
||||
|
||||
impl Update {
|
||||
/// Get the runtime version.
|
||||
pub fn runtime_version(&self) -> &RuntimeVersion {
|
||||
&self.runtime_version
|
||||
}
|
||||
|
||||
/// Get the metadata.
|
||||
pub fn metadata(&self) -> &Metadata {
|
||||
&self.metadata
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to wait until the runtime upgrade is applied on at finalized block.
|
||||
async fn wait_runtime_upgrade_in_finalized_block<T: Config>(
|
||||
client: &OnlineClient<T>,
|
||||
runtime_version: &RuntimeVersion,
|
||||
) -> Result<BlockRef<HashFor<T>>, RuntimeUpdaterError> {
|
||||
let hasher = client
|
||||
.inner
|
||||
.read()
|
||||
.expect("Lock shouldn't be poisoned")
|
||||
.hasher;
|
||||
|
||||
let mut block_sub = client
|
||||
.backend()
|
||||
.stream_finalized_block_headers(hasher)
|
||||
.await
|
||||
.map_err(RuntimeUpdaterError::CannotStreamFinalizedBlocks)?;
|
||||
|
||||
let block_ref = loop {
|
||||
let (_, block_ref) = block_sub
|
||||
.next()
|
||||
.await
|
||||
.ok_or(RuntimeUpdaterError::UnexpectedEndOfBlockStream)?
|
||||
.map_err(RuntimeUpdaterError::CannotGetNextFinalizedBlock)?;
|
||||
|
||||
let addr =
|
||||
crate::dynamic::storage::<(), scale_value::Value>("System", "LastRuntimeUpgrade");
|
||||
|
||||
let client_at = client.storage().at(block_ref.hash());
|
||||
let value = client_at
|
||||
.entry(addr)
|
||||
// The storage `system::lastRuntimeUpgrade` should always exist.
|
||||
// <https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/system/src/lib.rs#L958>
|
||||
.map_err(|_| RuntimeUpdaterError::CantFindSystemLastRuntimeUpgrade)?
|
||||
.fetch(())
|
||||
.await
|
||||
.map_err(RuntimeUpdaterError::CantFetchLastRuntimeUpgrade)?
|
||||
.decode_as::<LastRuntimeUpgrade>()
|
||||
.map_err(RuntimeUpdaterError::CannotDecodeLastRuntimeUpgrade)?;
|
||||
|
||||
#[derive(scale_decode::DecodeAsType)]
|
||||
struct LastRuntimeUpgrade {
|
||||
spec_version: u32,
|
||||
}
|
||||
|
||||
// We are waiting for the chain to have the same spec version
|
||||
// as sent out via the runtime subscription.
|
||||
if value.spec_version == runtime_version.spec_version {
|
||||
break block_ref;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(block_ref)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::{Config, client::OfflineClientT, error::ConstantError};
|
||||
use derive_where::derive_where;
|
||||
use pezkuwi_subxt_core::constants::address::Address;
|
||||
|
||||
/// A client for accessing constants.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct ConstantsClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> ConstantsClient<T, Client> {
|
||||
/// Create a new [`ConstantsClient`].
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, Client: OfflineClientT<T>> ConstantsClient<T, Client> {
|
||||
/// Run the validation logic against some constant address you'd like to access. Returns `Ok(())`
|
||||
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
|
||||
/// Return an error if the address was not valid or something went wrong trying to validate it (ie
|
||||
/// the pallet or constant in question do not exist at all).
|
||||
pub fn validate<Addr: Address>(&self, address: Addr) -> Result<(), ConstantError> {
|
||||
let metadata = self.client.metadata();
|
||||
pezkuwi_subxt_core::constants::validate(address, &metadata)
|
||||
}
|
||||
|
||||
/// Access the constant at the address given, returning the type defined by this address.
|
||||
/// This is probably used with addresses given from static codegen, although you can manually
|
||||
/// construct your own, too.
|
||||
pub fn at<Addr: Address>(&self, address: Addr) -> Result<Addr::Target, ConstantError> {
|
||||
let metadata = self.client.metadata();
|
||||
pezkuwi_subxt_core::constants::get(address, &metadata)
|
||||
}
|
||||
|
||||
/// Access the bytes of a constant by the address it is registered under.
|
||||
pub fn bytes_at<Addr: Address>(&self, address: Addr) -> Result<Vec<u8>, ConstantError> {
|
||||
let metadata = self.client.metadata();
|
||||
pezkuwi_subxt_core::constants::get_bytes(address, &metadata)
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Types associated with accessing constants.
|
||||
|
||||
mod constants_client;
|
||||
|
||||
pub use constants_client::ConstantsClient;
|
||||
pub use pezkuwi_subxt_core::constants::address::{Address, DynamicAddress, StaticAddress, dynamic};
|
||||
@@ -0,0 +1,134 @@
|
||||
use crate::client::OfflineClientT;
|
||||
use crate::{Config, error::CustomValueError};
|
||||
use derive_where::derive_where;
|
||||
|
||||
use pezkuwi_subxt_core::custom_values::address::{Address, Maybe};
|
||||
|
||||
/// A client for accessing custom values stored in the metadata.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct CustomValuesClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> CustomValuesClient<T, Client> {
|
||||
/// Create a new [`CustomValuesClient`].
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, Client: OfflineClientT<T>> CustomValuesClient<T, Client> {
|
||||
/// Access a custom value by the address it is registered under. This can be just a [str] to get back a dynamic value,
|
||||
/// or a static address from the generated static interface to get a value of a static type returned.
|
||||
pub fn at<Addr: Address<IsDecodable = Maybe>>(
|
||||
&self,
|
||||
address: Addr,
|
||||
) -> Result<Addr::Target, CustomValueError> {
|
||||
pezkuwi_subxt_core::custom_values::get(address, &self.client.metadata())
|
||||
}
|
||||
|
||||
/// Access the bytes of a custom value by the address it is registered under.
|
||||
pub fn bytes_at<Addr: Address>(&self, address: Addr) -> Result<Vec<u8>, CustomValueError> {
|
||||
pezkuwi_subxt_core::custom_values::get_bytes(address, &self.client.metadata())
|
||||
}
|
||||
|
||||
/// Run the validation logic against some custom value address you'd like to access. Returns `Ok(())`
|
||||
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
|
||||
/// Returns an error if the address was not valid (wrong name, type or raw bytes)
|
||||
pub fn validate<Addr: Address>(&self, address: Addr) -> Result<(), CustomValueError> {
|
||||
pezkuwi_subxt_core::custom_values::validate(address, &self.client.metadata())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::custom_values::{self, CustomValuesClient};
|
||||
use crate::{Metadata, OfflineClient, SubstrateConfig};
|
||||
use codec::Encode;
|
||||
use scale_decode::DecodeAsType;
|
||||
use scale_info::TypeInfo;
|
||||
use scale_info::form::PortableForm;
|
||||
use std::collections::BTreeMap;
|
||||
use pezkuwi_subxt_core::client::RuntimeVersion;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Encode, TypeInfo, DecodeAsType)]
|
||||
pub struct Person {
|
||||
age: u16,
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn mock_metadata() -> Metadata {
|
||||
let person_ty = scale_info::MetaType::new::<Person>();
|
||||
let unit = scale_info::MetaType::new::<()>();
|
||||
let mut types = scale_info::Registry::new();
|
||||
let person_ty_id = types.register_type(&person_ty);
|
||||
let unit_id = types.register_type(&unit);
|
||||
let types: scale_info::PortableRegistry = types.into();
|
||||
|
||||
let person = Person {
|
||||
age: 42,
|
||||
name: "Neo".into(),
|
||||
};
|
||||
|
||||
let person_value_metadata: frame_metadata::v15::CustomValueMetadata<PortableForm> =
|
||||
frame_metadata::v15::CustomValueMetadata {
|
||||
ty: person_ty_id,
|
||||
value: person.encode(),
|
||||
};
|
||||
|
||||
let frame_metadata = frame_metadata::v15::RuntimeMetadataV15 {
|
||||
types,
|
||||
pallets: vec![],
|
||||
extrinsic: frame_metadata::v15::ExtrinsicMetadata {
|
||||
version: 0,
|
||||
address_ty: unit_id,
|
||||
call_ty: unit_id,
|
||||
signature_ty: unit_id,
|
||||
extra_ty: unit_id,
|
||||
signed_extensions: vec![],
|
||||
},
|
||||
ty: unit_id,
|
||||
apis: vec![],
|
||||
outer_enums: frame_metadata::v15::OuterEnums {
|
||||
call_enum_ty: unit_id,
|
||||
event_enum_ty: unit_id,
|
||||
error_enum_ty: unit_id,
|
||||
},
|
||||
custom: frame_metadata::v15::CustomMetadata {
|
||||
map: BTreeMap::from_iter([("Person".to_string(), person_value_metadata)]),
|
||||
},
|
||||
};
|
||||
|
||||
let metadata: pezkuwi_subxt_metadata::Metadata = frame_metadata.try_into().unwrap();
|
||||
metadata
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decoding() {
|
||||
let client = OfflineClient::<SubstrateConfig>::new(
|
||||
Default::default(),
|
||||
RuntimeVersion {
|
||||
spec_version: 0,
|
||||
transaction_version: 0,
|
||||
},
|
||||
mock_metadata(),
|
||||
);
|
||||
|
||||
let custom_value_client = CustomValuesClient::new(client);
|
||||
assert!(custom_value_client.at("No one").is_err());
|
||||
|
||||
let person_addr = custom_values::dynamic::<Person>("Person");
|
||||
let person = custom_value_client.at(&person_addr).unwrap();
|
||||
assert_eq!(
|
||||
person,
|
||||
Person {
|
||||
age: 42,
|
||||
name: "Neo".into()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Types associated with accessing custom types
|
||||
|
||||
mod custom_values_client;
|
||||
|
||||
pub use custom_values_client::CustomValuesClient;
|
||||
pub use pezkuwi_subxt_core::custom_values::address::{Address, DynamicAddress, StaticAddress, dynamic};
|
||||
@@ -0,0 +1,358 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! A representation of the dispatch error; an error returned when
|
||||
//! something fails in trying to submit/execute a transaction.
|
||||
|
||||
use super::{DispatchErrorDecodeError, ModuleErrorDecodeError, ModuleErrorDetailsError};
|
||||
use crate::metadata::Metadata;
|
||||
use core::fmt::Debug;
|
||||
use scale_decode::{DecodeAsType, TypeResolver, visitor::DecodeAsTypeResult};
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
|
||||
/// An error dispatching a transaction.
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[non_exhaustive]
|
||||
pub enum DispatchError {
|
||||
/// Some error occurred.
|
||||
#[error("Some unknown error occurred.")]
|
||||
Other,
|
||||
/// Failed to lookup some data.
|
||||
#[error("Failed to lookup some data.")]
|
||||
CannotLookup,
|
||||
/// A bad origin.
|
||||
#[error("Bad origin.")]
|
||||
BadOrigin,
|
||||
/// A custom error in a module.
|
||||
#[error("Pallet error: {0}")]
|
||||
Module(ModuleError),
|
||||
/// At least one consumer is remaining so the account cannot be destroyed.
|
||||
#[error("At least one consumer is remaining so the account cannot be destroyed.")]
|
||||
ConsumerRemaining,
|
||||
/// There are no providers so the account cannot be created.
|
||||
#[error("There are no providers so the account cannot be created.")]
|
||||
NoProviders,
|
||||
/// There are too many consumers so the account cannot be created.
|
||||
#[error("There are too many consumers so the account cannot be created.")]
|
||||
TooManyConsumers,
|
||||
/// An error to do with tokens.
|
||||
#[error("Token error: {0}")]
|
||||
Token(TokenError),
|
||||
/// An arithmetic error.
|
||||
#[error("Arithmetic error: {0}")]
|
||||
Arithmetic(ArithmeticError),
|
||||
/// The number of transactional layers has been reached, or we are not in a transactional layer.
|
||||
#[error("Transactional error: {0}")]
|
||||
Transactional(TransactionalError),
|
||||
/// Resources exhausted, e.g. attempt to read/write data which is too large to manipulate.
|
||||
#[error(
|
||||
"Resources exhausted, e.g. attempt to read/write data which is too large to manipulate."
|
||||
)]
|
||||
Exhausted,
|
||||
/// The state is corrupt; this is generally not going to fix itself.
|
||||
#[error("The state is corrupt; this is generally not going to fix itself.")]
|
||||
Corruption,
|
||||
/// Some resource (e.g. a preimage) is unavailable right now. This might fix itself later.
|
||||
#[error(
|
||||
"Some resource (e.g. a preimage) is unavailable right now. This might fix itself later."
|
||||
)]
|
||||
Unavailable,
|
||||
/// Root origin is not allowed.
|
||||
#[error("Root origin is not allowed.")]
|
||||
RootNotAllowed,
|
||||
}
|
||||
|
||||
/// An error relating to tokens when dispatching a transaction.
|
||||
#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum TokenError {
|
||||
/// Funds are unavailable.
|
||||
#[error("Funds are unavailable.")]
|
||||
FundsUnavailable,
|
||||
/// Some part of the balance gives the only provider reference to the account and thus cannot be (re)moved.
|
||||
#[error(
|
||||
"Some part of the balance gives the only provider reference to the account and thus cannot be (re)moved."
|
||||
)]
|
||||
OnlyProvider,
|
||||
/// Account cannot exist with the funds that would be given.
|
||||
#[error("Account cannot exist with the funds that would be given.")]
|
||||
BelowMinimum,
|
||||
/// Account cannot be created.
|
||||
#[error("Account cannot be created.")]
|
||||
CannotCreate,
|
||||
/// The asset in question is unknown.
|
||||
#[error("The asset in question is unknown.")]
|
||||
UnknownAsset,
|
||||
/// Funds exist but are frozen.
|
||||
#[error("Funds exist but are frozen.")]
|
||||
Frozen,
|
||||
/// Operation is not supported by the asset.
|
||||
#[error("Operation is not supported by the asset.")]
|
||||
Unsupported,
|
||||
/// Account cannot be created for a held balance.
|
||||
#[error("Account cannot be created for a held balance.")]
|
||||
CannotCreateHold,
|
||||
/// Withdrawal would cause unwanted loss of account.
|
||||
#[error("Withdrawal would cause unwanted loss of account.")]
|
||||
NotExpendable,
|
||||
/// Account cannot receive the assets.
|
||||
#[error("Account cannot receive the assets.")]
|
||||
Blocked,
|
||||
}
|
||||
|
||||
/// An error relating to arithmetic when dispatching a transaction.
|
||||
#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ArithmeticError {
|
||||
/// Underflow.
|
||||
#[error("Underflow.")]
|
||||
Underflow,
|
||||
/// Overflow.
|
||||
#[error("Overflow.")]
|
||||
Overflow,
|
||||
/// Division by zero.
|
||||
#[error("Division by zero.")]
|
||||
DivisionByZero,
|
||||
}
|
||||
|
||||
/// An error relating to the transactional layers when dispatching a transaction.
|
||||
#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum TransactionalError {
|
||||
/// Too many transactional layers have been spawned.
|
||||
#[error("Too many transactional layers have been spawned.")]
|
||||
LimitReached,
|
||||
/// A transactional layer was expected, but does not exist.
|
||||
#[error("A transactional layer was expected, but does not exist.")]
|
||||
NoLayer,
|
||||
}
|
||||
|
||||
/// Details about a module error that has occurred.
|
||||
#[derive(Clone, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub struct ModuleError {
|
||||
metadata: Metadata,
|
||||
/// Bytes representation:
|
||||
/// - `bytes[0]`: pallet index
|
||||
/// - `bytes[1]`: error index
|
||||
/// - `bytes[2..]`: 3 bytes specific for the module error
|
||||
bytes: [u8; 5],
|
||||
}
|
||||
|
||||
impl PartialEq for ModuleError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// A module error is the same if the raw underlying details are the same.
|
||||
self.bytes == other.bytes
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ModuleError {}
|
||||
|
||||
/// Custom `Debug` implementation, ignores the very large `metadata` field, using it instead (as
|
||||
/// intended) to resolve the actual pallet and error names. This is much more useful for debugging.
|
||||
impl Debug for ModuleError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let details = self.details_string();
|
||||
write!(f, "ModuleError(<{details}>)")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ModuleError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let details = self.details_string();
|
||||
write!(f, "{details}")
|
||||
}
|
||||
}
|
||||
|
||||
impl ModuleError {
|
||||
/// Return more details about this error.
|
||||
pub fn details(&self) -> Result<ModuleErrorDetails<'_>, ModuleErrorDetailsError> {
|
||||
let pallet = self
|
||||
.metadata
|
||||
.pallet_by_error_index(self.pallet_index())
|
||||
.ok_or(ModuleErrorDetailsError::PalletNotFound {
|
||||
pallet_index: self.pallet_index(),
|
||||
})?;
|
||||
|
||||
let variant = pallet
|
||||
.error_variant_by_index(self.error_index())
|
||||
.ok_or_else(|| ModuleErrorDetailsError::ErrorVariantNotFound {
|
||||
pallet_name: pallet.name().into(),
|
||||
error_index: self.error_index(),
|
||||
})?;
|
||||
|
||||
Ok(ModuleErrorDetails { pallet, variant })
|
||||
}
|
||||
|
||||
/// Return a formatted string of the resolved error details for debugging/display purposes.
|
||||
pub fn details_string(&self) -> String {
|
||||
match self.details() {
|
||||
Ok(details) => format!(
|
||||
"{pallet_name}::{variant_name}",
|
||||
pallet_name = details.pallet.name(),
|
||||
variant_name = details.variant.name,
|
||||
),
|
||||
Err(_) => format!(
|
||||
"Unknown pallet error '{bytes:?}' (pallet and error details cannot be retrieved)",
|
||||
bytes = self.bytes
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the underlying module error data that was decoded.
|
||||
pub fn bytes(&self) -> [u8; 5] {
|
||||
self.bytes
|
||||
}
|
||||
|
||||
/// Obtain the pallet index from the underlying byte data.
|
||||
pub fn pallet_index(&self) -> u8 {
|
||||
self.bytes[0]
|
||||
}
|
||||
|
||||
/// Obtain the error index from the underlying byte data.
|
||||
pub fn error_index(&self) -> u8 {
|
||||
self.bytes[1]
|
||||
}
|
||||
|
||||
/// Attempts to decode the ModuleError into the top outer Error enum.
|
||||
pub fn as_root_error<E: DecodeAsType>(&self) -> Result<E, ModuleErrorDecodeError> {
|
||||
let decoded = E::decode_as_type(
|
||||
&mut &self.bytes[..],
|
||||
self.metadata.outer_enums().error_enum_ty(),
|
||||
self.metadata.types(),
|
||||
)
|
||||
.map_err(ModuleErrorDecodeError)?;
|
||||
|
||||
Ok(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
/// Details about the module error.
|
||||
pub struct ModuleErrorDetails<'a> {
|
||||
/// The pallet that the error is in
|
||||
pub pallet: pezkuwi_subxt_metadata::PalletMetadata<'a>,
|
||||
/// The variant representing the error
|
||||
pub variant: &'a scale_info::Variant<scale_info::form::PortableForm>,
|
||||
}
|
||||
|
||||
impl DispatchError {
|
||||
/// Attempt to decode a runtime [`DispatchError`].
|
||||
#[doc(hidden)]
|
||||
pub fn decode_from<'a>(
|
||||
bytes: impl Into<Cow<'a, [u8]>>,
|
||||
metadata: Metadata,
|
||||
) -> Result<Self, DispatchErrorDecodeError> {
|
||||
let bytes = bytes.into();
|
||||
let dispatch_error_ty_id = metadata
|
||||
.dispatch_error_ty()
|
||||
.ok_or(DispatchErrorDecodeError::DispatchErrorTypeIdNotFound)?;
|
||||
|
||||
// The aim is to decode our bytes into roughly this shape. This is copied from
|
||||
// `sp_runtime::DispatchError`; we need the variant names and any inner variant
|
||||
// names/shapes to line up in order for decoding to be successful.
|
||||
#[derive(scale_decode::DecodeAsType)]
|
||||
enum DecodedDispatchError {
|
||||
Other,
|
||||
CannotLookup,
|
||||
BadOrigin,
|
||||
Module(DecodedModuleErrorBytes),
|
||||
ConsumerRemaining,
|
||||
NoProviders,
|
||||
TooManyConsumers,
|
||||
Token(TokenError),
|
||||
Arithmetic(ArithmeticError),
|
||||
Transactional(TransactionalError),
|
||||
Exhausted,
|
||||
Corruption,
|
||||
Unavailable,
|
||||
RootNotAllowed,
|
||||
}
|
||||
|
||||
// ModuleError is a bit special; we want to support being decoded from either
|
||||
// a legacy format of 2 bytes, or a newer format of 5 bytes. So, just grab the bytes
|
||||
// out when decoding to manually work with them.
|
||||
struct DecodedModuleErrorBytes(Vec<u8>);
|
||||
struct DecodedModuleErrorBytesVisitor<R: TypeResolver>(PhantomData<R>);
|
||||
impl<R: TypeResolver> scale_decode::Visitor for DecodedModuleErrorBytesVisitor<R> {
|
||||
type Error = scale_decode::Error;
|
||||
type Value<'scale, 'info> = DecodedModuleErrorBytes;
|
||||
type TypeResolver = R;
|
||||
|
||||
fn unchecked_decode_as_type<'scale, 'info>(
|
||||
self,
|
||||
input: &mut &'scale [u8],
|
||||
_type_id: R::TypeId,
|
||||
_types: &'info R,
|
||||
) -> DecodeAsTypeResult<Self, Result<Self::Value<'scale, 'info>, Self::Error>>
|
||||
{
|
||||
DecodeAsTypeResult::Decoded(Ok(DecodedModuleErrorBytes(input.to_vec())))
|
||||
}
|
||||
}
|
||||
|
||||
impl scale_decode::IntoVisitor for DecodedModuleErrorBytes {
|
||||
type AnyVisitor<R: TypeResolver> = DecodedModuleErrorBytesVisitor<R>;
|
||||
fn into_visitor<R: TypeResolver>() -> DecodedModuleErrorBytesVisitor<R> {
|
||||
DecodedModuleErrorBytesVisitor(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
// Decode into our temporary error:
|
||||
let decoded_dispatch_err = DecodedDispatchError::decode_as_type(
|
||||
&mut &*bytes,
|
||||
dispatch_error_ty_id,
|
||||
metadata.types(),
|
||||
)
|
||||
.map_err(DispatchErrorDecodeError::CouldNotDecodeDispatchError)?;
|
||||
|
||||
// Convert into the outward-facing error, mainly by handling the Module variant.
|
||||
let dispatch_error = match decoded_dispatch_err {
|
||||
// Mostly we don't change anything from our decoded to our outward-facing error:
|
||||
DecodedDispatchError::Other => DispatchError::Other,
|
||||
DecodedDispatchError::CannotLookup => DispatchError::CannotLookup,
|
||||
DecodedDispatchError::BadOrigin => DispatchError::BadOrigin,
|
||||
DecodedDispatchError::ConsumerRemaining => DispatchError::ConsumerRemaining,
|
||||
DecodedDispatchError::NoProviders => DispatchError::NoProviders,
|
||||
DecodedDispatchError::TooManyConsumers => DispatchError::TooManyConsumers,
|
||||
DecodedDispatchError::Token(val) => DispatchError::Token(val),
|
||||
DecodedDispatchError::Arithmetic(val) => DispatchError::Arithmetic(val),
|
||||
DecodedDispatchError::Transactional(val) => DispatchError::Transactional(val),
|
||||
DecodedDispatchError::Exhausted => DispatchError::Exhausted,
|
||||
DecodedDispatchError::Corruption => DispatchError::Corruption,
|
||||
DecodedDispatchError::Unavailable => DispatchError::Unavailable,
|
||||
DecodedDispatchError::RootNotAllowed => DispatchError::RootNotAllowed,
|
||||
// But we apply custom logic to transform the module error into the outward facing version:
|
||||
DecodedDispatchError::Module(module_bytes) => {
|
||||
let module_bytes = module_bytes.0;
|
||||
|
||||
// The old version is 2 bytes; a pallet and error index.
|
||||
// The new version is 5 bytes; a pallet and error index and then 3 extra bytes.
|
||||
let bytes = if module_bytes.len() == 2 {
|
||||
[module_bytes[0], module_bytes[1], 0, 0, 0]
|
||||
} else if module_bytes.len() == 5 {
|
||||
[
|
||||
module_bytes[0],
|
||||
module_bytes[1],
|
||||
module_bytes[2],
|
||||
module_bytes[3],
|
||||
module_bytes[4],
|
||||
]
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Can't decode error sp_runtime::DispatchError: bytes do not match known shapes"
|
||||
);
|
||||
// Return _all_ of the bytes; every "unknown" return should be consistent.
|
||||
return Err(DispatchErrorDecodeError::CouldNotDecodeModuleError {
|
||||
bytes: bytes.to_vec(),
|
||||
});
|
||||
};
|
||||
|
||||
// And return our outward-facing version:
|
||||
DispatchError::Module(ModuleError { metadata, bytes })
|
||||
}
|
||||
};
|
||||
|
||||
Ok(dispatch_error)
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/// Display hex strings.
|
||||
#[derive(PartialEq, Eq, Clone, Debug, PartialOrd, Ord)]
|
||||
pub struct Hex(String);
|
||||
|
||||
impl<T: AsRef<[u8]>> From<T> for Hex {
|
||||
fn from(value: T) -> Self {
|
||||
Hex(hex::encode(value.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Hex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
+702
@@ -0,0 +1,702 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Types representing the errors that can be returned.
|
||||
|
||||
mod dispatch_error;
|
||||
mod hex;
|
||||
|
||||
crate::macros::cfg_unstable_light_client! {
|
||||
pub use pezkuwi_subxt_lightclient::LightClientError;
|
||||
}
|
||||
|
||||
// Re-export dispatch error types:
|
||||
pub use dispatch_error::{
|
||||
ArithmeticError, DispatchError, ModuleError, TokenError, TransactionalError,
|
||||
};
|
||||
|
||||
// Re-expose the errors we use from other crates here:
|
||||
pub use crate::Metadata;
|
||||
pub use hex::Hex;
|
||||
pub use scale_decode::Error as DecodeError;
|
||||
pub use scale_encode::Error as EncodeError;
|
||||
pub use pezkuwi_subxt_metadata::TryFromError as MetadataTryFromError;
|
||||
|
||||
// Re-export core error types we're just reusing.
|
||||
pub use pezkuwi_subxt_core::error::{
|
||||
ConstantError,
|
||||
CustomValueError,
|
||||
EventsError as CoreEventsError,
|
||||
// These errors are exposed as-is:
|
||||
ExtrinsicDecodeErrorAt,
|
||||
// These errors are wrapped:
|
||||
ExtrinsicError as CoreExtrinsicError,
|
||||
RuntimeApiError as CoreRuntimeApiError,
|
||||
StorageError as CoreStorageError,
|
||||
StorageKeyError,
|
||||
StorageValueError,
|
||||
ViewFunctionError as CoreViewFunctionError,
|
||||
};
|
||||
|
||||
/// A global error type. Any of the errors exposed here can convert into this
|
||||
/// error via `.into()`, but this error isn't itself exposed from anything.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
ExtrinsicDecodeErrorAt(#[from] ExtrinsicDecodeErrorAt),
|
||||
#[error(transparent)]
|
||||
ConstantError(#[from] ConstantError),
|
||||
#[error(transparent)]
|
||||
CustomValueError(#[from] CustomValueError),
|
||||
#[error(transparent)]
|
||||
StorageKeyError(#[from] StorageKeyError),
|
||||
#[error(transparent)]
|
||||
StorageValueError(#[from] StorageValueError),
|
||||
#[error(transparent)]
|
||||
BackendError(#[from] BackendError),
|
||||
#[error(transparent)]
|
||||
BlockError(#[from] BlockError),
|
||||
#[error(transparent)]
|
||||
AccountNonceError(#[from] AccountNonceError),
|
||||
#[error(transparent)]
|
||||
OnlineClientError(#[from] OnlineClientError),
|
||||
#[error(transparent)]
|
||||
RuntimeUpdaterError(#[from] RuntimeUpdaterError),
|
||||
#[error(transparent)]
|
||||
RuntimeUpdateeApplyError(#[from] RuntimeUpdateeApplyError),
|
||||
#[error(transparent)]
|
||||
RuntimeApiError(#[from] RuntimeApiError),
|
||||
#[error(transparent)]
|
||||
EventsError(#[from] EventsError),
|
||||
#[error(transparent)]
|
||||
ExtrinsicError(#[from] ExtrinsicError),
|
||||
#[error(transparent)]
|
||||
ViewFunctionError(#[from] ViewFunctionError),
|
||||
#[error(transparent)]
|
||||
TransactionProgressError(#[from] TransactionProgressError),
|
||||
#[error(transparent)]
|
||||
TransactionStatusError(#[from] TransactionStatusError),
|
||||
#[error(transparent)]
|
||||
TransactionEventsError(#[from] TransactionEventsError),
|
||||
#[error(transparent)]
|
||||
TransactionFinalizedSuccessError(#[from] TransactionFinalizedSuccessError),
|
||||
#[error(transparent)]
|
||||
ModuleErrorDetailsError(#[from] ModuleErrorDetailsError),
|
||||
#[error(transparent)]
|
||||
ModuleErrorDecodeError(#[from] ModuleErrorDecodeError),
|
||||
#[error(transparent)]
|
||||
DispatchErrorDecodeError(#[from] DispatchErrorDecodeError),
|
||||
#[error(transparent)]
|
||||
StorageError(#[from] StorageError),
|
||||
// Dev note: Subxt doesn't directly return Raw* errors. These exist so that when
|
||||
// users use common crates (like parity-scale-codec and subxt-rpcs), errors returned
|
||||
// there can be handled automatically using ? when the expected error is subxt::Error.
|
||||
#[error("Other RPC client error: {0}")]
|
||||
OtherRpcClientError(#[from] pezkuwi_subxt_rpcs::Error),
|
||||
#[error("Other codec error: {0}")]
|
||||
OtherCodecError(#[from] codec::Error),
|
||||
#[cfg(feature = "unstable-light-client")]
|
||||
#[error("Other light client error: {0}")]
|
||||
OtherLightClientError(#[from] pezkuwi_subxt_lightclient::LightClientError),
|
||||
#[cfg(feature = "unstable-light-client")]
|
||||
#[error("Other light client RPC error: {0}")]
|
||||
OtherLightClientRpcError(#[from] pezkuwi_subxt_lightclient::LightClientRpcError),
|
||||
// Dev note: Nothing in subxt should ever emit this error. It can instead be used
|
||||
// to easily map other errors into a subxt::Error for convenience. Some From impls
|
||||
// make this automatic for common "other" error types.
|
||||
#[error("Other error: {0}")]
|
||||
Other(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
impl From<std::convert::Infallible> for Error {
|
||||
fn from(value: std::convert::Infallible) -> Self {
|
||||
match value {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Create a generic error. This is a quick workaround when you are using
|
||||
/// [`Error`] and have a non-Subxt error to return.
|
||||
pub fn other<E: std::error::Error + Send + Sync + 'static>(error: E) -> Error {
|
||||
Error::Other(Box::new(error))
|
||||
}
|
||||
|
||||
/// Create a generic error from a string. This is a quick workaround when you are using
|
||||
/// [`Error`] and have a non-Subxt error to return.
|
||||
pub fn other_str(error: impl Into<String>) -> Error {
|
||||
#[derive(thiserror::Error, Debug, Clone)]
|
||||
#[error("{0}")]
|
||||
struct StrError(String);
|
||||
Error::Other(Box::new(StrError(error.into())))
|
||||
}
|
||||
|
||||
/// Checks whether the error was caused by a RPC re-connection.
|
||||
pub fn is_disconnected_will_reconnect(&self) -> bool {
|
||||
matches!(
|
||||
self.backend_error(),
|
||||
Some(BackendError::Rpc(RpcError::ClientError(
|
||||
pezkuwi_subxt_rpcs::Error::DisconnectedWillReconnect(_)
|
||||
)))
|
||||
)
|
||||
}
|
||||
|
||||
/// Checks whether the error was caused by a RPC request being rejected.
|
||||
pub fn is_rpc_limit_reached(&self) -> bool {
|
||||
matches!(
|
||||
self.backend_error(),
|
||||
Some(BackendError::Rpc(RpcError::LimitReached))
|
||||
)
|
||||
}
|
||||
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
Error::BlockError(e) => e.backend_error(),
|
||||
Error::AccountNonceError(e) => e.backend_error(),
|
||||
Error::OnlineClientError(e) => e.backend_error(),
|
||||
Error::RuntimeUpdaterError(e) => e.backend_error(),
|
||||
Error::RuntimeApiError(e) => e.backend_error(),
|
||||
Error::EventsError(e) => e.backend_error(),
|
||||
Error::ExtrinsicError(e) => e.backend_error(),
|
||||
Error::ViewFunctionError(e) => e.backend_error(),
|
||||
Error::TransactionProgressError(e) => e.backend_error(),
|
||||
Error::TransactionEventsError(e) => e.backend_error(),
|
||||
Error::TransactionFinalizedSuccessError(e) => e.backend_error(),
|
||||
Error::StorageError(e) => e.backend_error(),
|
||||
// Any errors that **don't** return a BackendError anywhere will return None:
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum BackendError {
|
||||
#[error("Backend error: RPC error: {0}")]
|
||||
Rpc(#[from] RpcError),
|
||||
#[error("Backend error: Could not find metadata version {0}")]
|
||||
MetadataVersionNotFound(u32),
|
||||
#[error("Backend error: Could not codec::Decode Runtime API response: {0}")]
|
||||
CouldNotScaleDecodeRuntimeResponse(codec::Error),
|
||||
#[error("Backend error: Could not codec::Decode metadata bytes into subxt::Metadata: {0}")]
|
||||
CouldNotDecodeMetadata(codec::Error),
|
||||
// This is for errors in `Backend` implementations which aren't any of the "pre-defined" set above:
|
||||
#[error("Custom backend error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl BackendError {
|
||||
/// Checks whether the error was caused by a RPC re-connection.
|
||||
pub fn is_disconnected_will_reconnect(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
BackendError::Rpc(RpcError::ClientError(
|
||||
pezkuwi_subxt_rpcs::Error::DisconnectedWillReconnect(_)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
/// Checks whether the error was caused by a RPC request being rejected.
|
||||
pub fn is_rpc_limit_reached(&self) -> bool {
|
||||
matches!(self, BackendError::Rpc(RpcError::LimitReached))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pezkuwi_subxt_rpcs::Error> for BackendError {
|
||||
fn from(value: pezkuwi_subxt_rpcs::Error) -> Self {
|
||||
BackendError::Rpc(RpcError::ClientError(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// An RPC error. Since we are generic over the RPC client that is used,
|
||||
/// the error is boxed and could be casted.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum RpcError {
|
||||
/// Error related to the RPC client.
|
||||
#[error("RPC error: {0}")]
|
||||
ClientError(#[from] pezkuwi_subxt_rpcs::Error),
|
||||
/// This error signals that we got back a [`pezkuwi_subxt_rpcs::methods::chain_head::MethodResponse::LimitReached`],
|
||||
/// which is not technically an RPC error but is treated as an error in our own APIs.
|
||||
#[error("RPC error: limit reached")]
|
||||
LimitReached,
|
||||
/// The RPC subscription was dropped.
|
||||
#[error("RPC error: subscription dropped.")]
|
||||
SubscriptionDropped,
|
||||
}
|
||||
|
||||
/// Block error
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum BlockError {
|
||||
#[error(
|
||||
"Could not find the block body with hash {block_hash} (perhaps it was on a non-finalized fork?)"
|
||||
)]
|
||||
BlockNotFound { block_hash: Hex },
|
||||
#[error("Could not download the block header with hash {block_hash}: {reason}")]
|
||||
CouldNotGetBlockHeader {
|
||||
block_hash: Hex,
|
||||
reason: BackendError,
|
||||
},
|
||||
#[error("Could not download the latest block header: {0}")]
|
||||
CouldNotGetLatestBlock(BackendError),
|
||||
#[error("Could not subscribe to all blocks: {0}")]
|
||||
CouldNotSubscribeToAllBlocks(BackendError),
|
||||
#[error("Could not subscribe to best blocks: {0}")]
|
||||
CouldNotSubscribeToBestBlocks(BackendError),
|
||||
#[error("Could not subscribe to finalized blocks: {0}")]
|
||||
CouldNotSubscribeToFinalizedBlocks(BackendError),
|
||||
#[error("Error getting account nonce at block {block_hash}")]
|
||||
AccountNonceError {
|
||||
block_hash: Hex,
|
||||
account_id: Hex,
|
||||
reason: AccountNonceError,
|
||||
},
|
||||
}
|
||||
|
||||
impl BlockError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
BlockError::CouldNotGetBlockHeader { reason: e, .. }
|
||||
| BlockError::CouldNotGetLatestBlock(e)
|
||||
| BlockError::CouldNotSubscribeToAllBlocks(e)
|
||||
| BlockError::CouldNotSubscribeToBestBlocks(e)
|
||||
| BlockError::CouldNotSubscribeToFinalizedBlocks(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum AccountNonceError {
|
||||
#[error("Could not retrieve account nonce: {0}")]
|
||||
CouldNotRetrieve(#[from] BackendError),
|
||||
#[error("Could not decode account nonce: {0}")]
|
||||
CouldNotDecode(#[from] codec::Error),
|
||||
#[error("Wrong number of account nonce bytes returned: {0} (expected 2, 4 or 8)")]
|
||||
WrongNumberOfBytes(usize),
|
||||
}
|
||||
|
||||
impl AccountNonceError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
AccountNonceError::CouldNotRetrieve(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum OnlineClientError {
|
||||
#[error("Cannot construct OnlineClient: {0}")]
|
||||
RpcError(#[from] pezkuwi_subxt_rpcs::Error),
|
||||
#[error(
|
||||
"Cannot construct OnlineClient: Cannot fetch latest finalized block to obtain init details from: {0}"
|
||||
)]
|
||||
CannotGetLatestFinalizedBlock(BackendError),
|
||||
#[error("Cannot construct OnlineClient: Cannot fetch genesis hash: {0}")]
|
||||
CannotGetGenesisHash(BackendError),
|
||||
#[error("Cannot construct OnlineClient: Cannot fetch current runtime version: {0}")]
|
||||
CannotGetCurrentRuntimeVersion(BackendError),
|
||||
#[error("Cannot construct OnlineClient: Cannot fetch metadata: {0}")]
|
||||
CannotFetchMetadata(BackendError),
|
||||
}
|
||||
|
||||
impl OnlineClientError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
OnlineClientError::CannotGetLatestFinalizedBlock(e)
|
||||
| OnlineClientError::CannotGetGenesisHash(e)
|
||||
| OnlineClientError::CannotGetCurrentRuntimeVersion(e)
|
||||
| OnlineClientError::CannotFetchMetadata(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum RuntimeUpdaterError {
|
||||
#[error("Error subscribing to runtime updates: The update stream ended unexpectedly")]
|
||||
UnexpectedEndOfUpdateStream,
|
||||
#[error("Error subscribing to runtime updates: The finalized block stream ended unexpectedly")]
|
||||
UnexpectedEndOfBlockStream,
|
||||
#[error("Error subscribing to runtime updates: Can't stream runtime version: {0}")]
|
||||
CannotStreamRuntimeVersion(BackendError),
|
||||
#[error("Error subscribing to runtime updates: Can't get next runtime version in stream: {0}")]
|
||||
CannotGetNextRuntimeVersion(BackendError),
|
||||
#[error("Error subscribing to runtime updates: Cannot stream finalized blocks: {0}")]
|
||||
CannotStreamFinalizedBlocks(BackendError),
|
||||
#[error("Error subscribing to runtime updates: Cannot get next finalized block in stream: {0}")]
|
||||
CannotGetNextFinalizedBlock(BackendError),
|
||||
#[error("Cannot fetch new metadata for runtime update: {0}")]
|
||||
CannotFetchNewMetadata(BackendError),
|
||||
#[error(
|
||||
"Error subscribing to runtime updates: Cannot find the System.LastRuntimeUpgrade storage entry"
|
||||
)]
|
||||
CantFindSystemLastRuntimeUpgrade,
|
||||
#[error("Error subscribing to runtime updates: Cannot fetch last runtime upgrade: {0}")]
|
||||
CantFetchLastRuntimeUpgrade(StorageError),
|
||||
#[error("Error subscribing to runtime updates: Cannot decode last runtime upgrade: {0}")]
|
||||
CannotDecodeLastRuntimeUpgrade(StorageValueError),
|
||||
}
|
||||
|
||||
impl RuntimeUpdaterError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
RuntimeUpdaterError::CannotStreamRuntimeVersion(e)
|
||||
| RuntimeUpdaterError::CannotGetNextRuntimeVersion(e)
|
||||
| RuntimeUpdaterError::CannotStreamFinalizedBlocks(e)
|
||||
| RuntimeUpdaterError::CannotGetNextFinalizedBlock(e)
|
||||
| RuntimeUpdaterError::CannotFetchNewMetadata(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error that can occur during upgrade.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum RuntimeUpdateeApplyError {
|
||||
#[error("The proposed runtime update is the same as the current version")]
|
||||
SameVersion,
|
||||
}
|
||||
|
||||
/// Error working with Runtime APIs
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum RuntimeApiError {
|
||||
#[error("Cannot access Runtime APIs at latest block: Cannot fetch latest finalized block: {0}")]
|
||||
CannotGetLatestFinalizedBlock(BackendError),
|
||||
#[error("{0}")]
|
||||
OfflineError(#[from] CoreRuntimeApiError),
|
||||
#[error("Cannot call the Runtime API: {0}")]
|
||||
CannotCallApi(BackendError),
|
||||
}
|
||||
|
||||
impl RuntimeApiError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
RuntimeApiError::CannotGetLatestFinalizedBlock(e)
|
||||
| RuntimeApiError::CannotCallApi(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error working with events.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum EventsError {
|
||||
#[error("{0}")]
|
||||
OfflineError(#[from] CoreEventsError),
|
||||
#[error("Cannot access events at latest block: Cannot fetch latest finalized block: {0}")]
|
||||
CannotGetLatestFinalizedBlock(BackendError),
|
||||
#[error("Cannot fetch event bytes: {0}")]
|
||||
CannotFetchEventBytes(BackendError),
|
||||
}
|
||||
|
||||
impl EventsError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
EventsError::CannotGetLatestFinalizedBlock(e)
|
||||
| EventsError::CannotFetchEventBytes(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error working with extrinsics.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ExtrinsicError {
|
||||
#[error("{0}")]
|
||||
OfflineError(#[from] CoreExtrinsicError),
|
||||
#[error("Could not download block body to extract extrinsics from: {0}")]
|
||||
CannotGetBlockBody(BackendError),
|
||||
#[error("Block not found: {0}")]
|
||||
BlockNotFound(Hex),
|
||||
#[error("{0}")]
|
||||
CouldNotDecodeExtrinsics(#[from] ExtrinsicDecodeErrorAt),
|
||||
#[error(
|
||||
"Extrinsic submission error: Cannot get latest finalized block to grab account nonce at: {0}"
|
||||
)]
|
||||
CannotGetLatestFinalizedBlock(BackendError),
|
||||
#[error("Cannot find block header for block {block_hash}")]
|
||||
CannotFindBlockHeader { block_hash: Hex },
|
||||
#[error("Error getting account nonce at block {block_hash}")]
|
||||
AccountNonceError {
|
||||
block_hash: Hex,
|
||||
account_id: Hex,
|
||||
reason: AccountNonceError,
|
||||
},
|
||||
#[error("Cannot submit extrinsic: {0}")]
|
||||
ErrorSubmittingTransaction(BackendError),
|
||||
#[error("A transaction status error was returned while submitting the extrinsic: {0}")]
|
||||
TransactionStatusError(TransactionStatusError),
|
||||
#[error(
|
||||
"The transaction status stream encountered an error while submitting the extrinsic: {0}"
|
||||
)]
|
||||
TransactionStatusStreamError(BackendError),
|
||||
#[error(
|
||||
"The transaction status stream unexpectedly ended, so we don't know the status of the submitted extrinsic"
|
||||
)]
|
||||
UnexpectedEndOfTransactionStatusStream,
|
||||
#[error("Cannot get fee info from Runtime API: {0}")]
|
||||
CannotGetFeeInfo(BackendError),
|
||||
#[error("Cannot get validation info from Runtime API: {0}")]
|
||||
CannotGetValidationInfo(BackendError),
|
||||
#[error("Cannot decode ValidationResult bytes: {0}")]
|
||||
CannotDecodeValidationResult(codec::Error),
|
||||
#[error("ValidationResult bytes could not be decoded")]
|
||||
UnexpectedValidationResultBytes(Vec<u8>),
|
||||
}
|
||||
|
||||
impl ExtrinsicError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
ExtrinsicError::CannotGetBlockBody(e)
|
||||
| ExtrinsicError::CannotGetLatestFinalizedBlock(e)
|
||||
| ExtrinsicError::ErrorSubmittingTransaction(e)
|
||||
| ExtrinsicError::TransactionStatusStreamError(e)
|
||||
| ExtrinsicError::CannotGetFeeInfo(e)
|
||||
| ExtrinsicError::CannotGetValidationInfo(e) => Some(e),
|
||||
ExtrinsicError::AccountNonceError { reason, .. } => reason.backend_error(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error working with View Functions.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ViewFunctionError {
|
||||
#[error("{0}")]
|
||||
OfflineError(#[from] CoreViewFunctionError),
|
||||
#[error(
|
||||
"Cannot access View Functions at latest block: Cannot fetch latest finalized block: {0}"
|
||||
)]
|
||||
CannotGetLatestFinalizedBlock(BackendError),
|
||||
#[error("Cannot call the View Function Runtime API: {0}")]
|
||||
CannotCallApi(BackendError),
|
||||
}
|
||||
|
||||
impl ViewFunctionError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
ViewFunctionError::CannotGetLatestFinalizedBlock(e)
|
||||
| ViewFunctionError::CannotCallApi(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error during the transaction progress.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum TransactionProgressError {
|
||||
#[error("Cannot get the next transaction progress update: {0}")]
|
||||
CannotGetNextProgressUpdate(BackendError),
|
||||
#[error("Error during transaction progress: {0}")]
|
||||
TransactionStatusError(#[from] TransactionStatusError),
|
||||
#[error(
|
||||
"The transaction status stream unexpectedly ended, so we have no further transaction progress updates"
|
||||
)]
|
||||
UnexpectedEndOfTransactionStatusStream,
|
||||
}
|
||||
|
||||
impl TransactionProgressError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
TransactionProgressError::CannotGetNextProgressUpdate(e) => Some(e),
|
||||
TransactionProgressError::TransactionStatusError(_) => None,
|
||||
TransactionProgressError::UnexpectedEndOfTransactionStatusStream => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error emitted as the result of a transaction progress update.
|
||||
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum TransactionStatusError {
|
||||
/// An error happened on the node that the transaction was submitted to.
|
||||
#[error("Error handling transaction: {0}")]
|
||||
Error(String),
|
||||
/// The transaction was deemed invalid.
|
||||
#[error("The transaction is not valid: {0}")]
|
||||
Invalid(String),
|
||||
/// The transaction was dropped.
|
||||
#[error("The transaction was dropped: {0}")]
|
||||
Dropped(String),
|
||||
}
|
||||
|
||||
/// Error fetching events for a just-submitted transaction
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum TransactionEventsError {
|
||||
#[error(
|
||||
"The block containing the submitted transaction ({block_hash}) could not be downloaded: {error}"
|
||||
)]
|
||||
CannotFetchBlockBody {
|
||||
block_hash: Hex,
|
||||
error: BackendError,
|
||||
},
|
||||
#[error(
|
||||
"Cannot find the the submitted transaction (hash: {transaction_hash}) in the block (hash: {block_hash}) it is supposed to be in."
|
||||
)]
|
||||
CannotFindTransactionInBlock {
|
||||
block_hash: Hex,
|
||||
transaction_hash: Hex,
|
||||
},
|
||||
#[error("The block containing the submitted transaction ({block_hash}) could not be found")]
|
||||
BlockNotFound { block_hash: Hex },
|
||||
#[error(
|
||||
"Could not decode event at index {event_index} for the submitted transaction at block {block_hash}: {error}"
|
||||
)]
|
||||
CannotDecodeEventInBlock {
|
||||
event_index: usize,
|
||||
block_hash: Hex,
|
||||
error: EventsError,
|
||||
},
|
||||
#[error("Could not fetch events for the submitted transaction: {error}")]
|
||||
CannotFetchEventsForTransaction {
|
||||
block_hash: Hex,
|
||||
transaction_hash: Hex,
|
||||
error: EventsError,
|
||||
},
|
||||
#[error("The transaction led to a DispatchError, but we failed to decode it: {error}")]
|
||||
CannotDecodeDispatchError {
|
||||
error: DispatchErrorDecodeError,
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
#[error("The transaction failed with the following dispatch error: {0}")]
|
||||
ExtrinsicFailed(#[from] DispatchError),
|
||||
}
|
||||
|
||||
impl TransactionEventsError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
TransactionEventsError::CannotFetchBlockBody { error, .. } => Some(error),
|
||||
TransactionEventsError::CannotDecodeEventInBlock { error, .. }
|
||||
| TransactionEventsError::CannotFetchEventsForTransaction { error, .. } => {
|
||||
error.backend_error()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error waiting for the transaction to be finalized and successful.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs, clippy::large_enum_variant)]
|
||||
pub enum TransactionFinalizedSuccessError {
|
||||
#[error("Could not finalize the transaction: {0}")]
|
||||
FinalizationError(#[from] TransactionProgressError),
|
||||
#[error("The transaction did not succeed: {0}")]
|
||||
SuccessError(#[from] TransactionEventsError),
|
||||
}
|
||||
|
||||
impl TransactionFinalizedSuccessError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
TransactionFinalizedSuccessError::FinalizationError(e) => e.backend_error(),
|
||||
TransactionFinalizedSuccessError::SuccessError(e) => e.backend_error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error decoding the [`DispatchError`]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ModuleErrorDetailsError {
|
||||
#[error(
|
||||
"Could not get details for the DispatchError: could not find pallet index {pallet_index}"
|
||||
)]
|
||||
PalletNotFound { pallet_index: u8 },
|
||||
#[error(
|
||||
"Could not get details for the DispatchError: could not find error index {error_index} in pallet {pallet_name}"
|
||||
)]
|
||||
ErrorVariantNotFound {
|
||||
pallet_name: String,
|
||||
error_index: u8,
|
||||
},
|
||||
}
|
||||
|
||||
/// Error decoding the [`ModuleError`]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
#[error("Could not decode the DispatchError::Module payload into the given type: {0}")]
|
||||
pub struct ModuleErrorDecodeError(scale_decode::Error);
|
||||
|
||||
/// Error decoding the [`DispatchError`]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum DispatchErrorDecodeError {
|
||||
#[error(
|
||||
"Could not decode the DispatchError: could not find the corresponding type ID in the metadata"
|
||||
)]
|
||||
DispatchErrorTypeIdNotFound,
|
||||
#[error("Could not decode the DispatchError: {0}")]
|
||||
CouldNotDecodeDispatchError(scale_decode::Error),
|
||||
#[error("Could not decode the DispatchError::Module variant")]
|
||||
CouldNotDecodeModuleError {
|
||||
/// The bytes corresponding to the Module variant we were unable to decode:
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Error working with storage.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum StorageError {
|
||||
#[error("{0}")]
|
||||
Offline(#[from] CoreStorageError),
|
||||
#[error("Cannot access storage at latest block: Cannot fetch latest finalized block: {0}")]
|
||||
CannotGetLatestFinalizedBlock(BackendError),
|
||||
#[error(
|
||||
"No storage value found at the given address, and no default value to fall back to using."
|
||||
)]
|
||||
NoValueFound,
|
||||
#[error("Cannot fetch the storage value: {0}")]
|
||||
CannotFetchValue(BackendError),
|
||||
#[error("Cannot iterate storage values: {0}")]
|
||||
CannotIterateValues(BackendError),
|
||||
#[error("Encountered an error iterating over storage values: {0}")]
|
||||
StreamFailure(BackendError),
|
||||
#[error("Cannot decode the storage version for a given entry: {0}")]
|
||||
CannotDecodeStorageVersion(codec::Error),
|
||||
}
|
||||
|
||||
impl StorageError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
StorageError::CannotGetLatestFinalizedBlock(e)
|
||||
| StorageError::CannotFetchValue(e)
|
||||
| StorageError::CannotIterateValues(e)
|
||||
| StorageError::StreamFailure(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2019-2025 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::{Backend, BackendExt, BlockRef};
|
||||
use crate::{
|
||||
client::OnlineClientT,
|
||||
config::{Config, HashFor},
|
||||
error::EventsError,
|
||||
events::Events,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use std::future::Future;
|
||||
|
||||
/// A client for working with events.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct EventsClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> EventsClient<T, Client> {
|
||||
/// Create a new [`EventsClient`].
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> EventsClient<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Obtain events at some block hash.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This call only supports blocks produced since the most recent
|
||||
/// runtime upgrade. You can attempt to retrieve events from older blocks,
|
||||
/// but may run into errors attempting to work with them.
|
||||
pub fn at(
|
||||
&self,
|
||||
block_ref: impl Into<BlockRef<HashFor<T>>>,
|
||||
) -> impl Future<Output = Result<Events<T>, EventsError>> + Send + 'static {
|
||||
self.at_or_latest(Some(block_ref.into()))
|
||||
}
|
||||
|
||||
/// Obtain events for the latest finalized block.
|
||||
pub fn at_latest(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<Events<T>, EventsError>> + Send + 'static {
|
||||
self.at_or_latest(None)
|
||||
}
|
||||
|
||||
/// Obtain events at some block hash.
|
||||
fn at_or_latest(
|
||||
&self,
|
||||
block_ref: Option<BlockRef<HashFor<T>>>,
|
||||
) -> impl Future<Output = Result<Events<T>, EventsError>> + Send + 'static {
|
||||
// Clone and pass the client in like this so that we can explicitly
|
||||
// return a Future that's Send + 'static, rather than tied to &self.
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
// If a block ref isn't provided, we'll get the latest finalized block to use.
|
||||
let block_ref = match block_ref {
|
||||
Some(r) => r,
|
||||
None => client
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(EventsError::CannotGetLatestFinalizedBlock)?,
|
||||
};
|
||||
|
||||
let event_bytes = get_event_bytes(client.backend(), block_ref.hash()).await?;
|
||||
Ok(Events::decode_from(event_bytes, client.metadata()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The storage key needed to access events.
|
||||
fn system_events_key() -> [u8; 32] {
|
||||
let a = pezsp_crypto_hashing::twox_128(b"System");
|
||||
let b = pezsp_crypto_hashing::twox_128(b"Events");
|
||||
let mut res = [0; 32];
|
||||
res[0..16].clone_from_slice(&a);
|
||||
res[16..32].clone_from_slice(&b);
|
||||
res
|
||||
}
|
||||
|
||||
// Get the event bytes from the provided client, at the provided block hash.
|
||||
pub(crate) async fn get_event_bytes<T: Config>(
|
||||
backend: &dyn Backend<T>,
|
||||
block_hash: HashFor<T>,
|
||||
) -> Result<Vec<u8>, EventsError> {
|
||||
let bytes = backend
|
||||
.storage_fetch_value(system_events_key().to_vec(), block_hash)
|
||||
.await
|
||||
.map_err(EventsError::CannotFetchEventBytes)?
|
||||
.unwrap_or_default();
|
||||
Ok(bytes)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
use crate::{
|
||||
Metadata,
|
||||
config::{Config, HashFor},
|
||||
error::EventsError,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use scale_decode::{DecodeAsFields, DecodeAsType};
|
||||
use pezkuwi_subxt_core::events::{EventDetails as CoreEventDetails, Events as CoreEvents};
|
||||
|
||||
pub use pezkuwi_subxt_core::events::{EventMetadataDetails, Phase, StaticEvent};
|
||||
|
||||
/// A collection of events obtained from a block, bundled with the necessary
|
||||
/// information needed to decode and iterate over them.
|
||||
// Dev note: we are just wrapping the pezkuwi_subxt_core types here to avoid leaking them
|
||||
// in Subxt and map any errors into Subxt errors so that we don't have this part of the
|
||||
// API returning a different error type (ie the pezkuwi_subxt_core::Error).
|
||||
#[derive_where(Clone, Debug)]
|
||||
pub struct Events<T> {
|
||||
inner: CoreEvents<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Events<T> {
|
||||
/// Create a new [`Events`] instance from the given bytes.
|
||||
pub fn decode_from(event_bytes: Vec<u8>, metadata: Metadata) -> Self {
|
||||
Self {
|
||||
inner: CoreEvents::decode_from(event_bytes, metadata),
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of events.
|
||||
pub fn len(&self) -> u32 {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
/// Are there no events in this block?
|
||||
// Note: mainly here to satisfy clippy..
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// Return the bytes representing all of the events.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
self.inner.bytes()
|
||||
}
|
||||
|
||||
/// Iterate over all of the events, using metadata to dynamically
|
||||
/// decode them as we go, and returning the raw bytes and other associated
|
||||
/// details. If an error occurs, all subsequent iterations return `None`.
|
||||
// Dev note: The returned iterator is 'static + Send so that we can box it up and make
|
||||
// use of it with our `FilterEvents` stuff.
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<EventDetails<T>, EventsError>> + Send + Sync + 'static {
|
||||
self.inner
|
||||
.iter()
|
||||
.map(|item| item.map(|e| EventDetails { inner: e }).map_err(Into::into))
|
||||
}
|
||||
|
||||
/// Iterate through the events using metadata to dynamically decode and skip
|
||||
/// them, and return only those which should decode to the provided `Ev` type.
|
||||
/// If an error occurs, all subsequent iterations return `None`.
|
||||
pub fn find<Ev: StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, EventsError>> {
|
||||
self.inner.find::<Ev>().map(|item| item.map_err(Into::into))
|
||||
}
|
||||
|
||||
/// Iterate through the events using metadata to dynamically decode and skip
|
||||
/// them, and return the first event found which decodes to the provided `Ev` type.
|
||||
pub fn find_first<Ev: StaticEvent>(&self) -> Result<Option<Ev>, EventsError> {
|
||||
self.inner.find_first::<Ev>().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Iterate through the events using metadata to dynamically decode and skip
|
||||
/// them, and return the last event found which decodes to the provided `Ev` type.
|
||||
pub fn find_last<Ev: StaticEvent>(&self) -> Result<Option<Ev>, EventsError> {
|
||||
self.inner.find_last::<Ev>().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find an event that decodes to the type provided. Returns true if it was found.
|
||||
pub fn has<Ev: StaticEvent>(&self) -> Result<bool, EventsError> {
|
||||
self.inner.has::<Ev>().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// The event details.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventDetails<T: Config> {
|
||||
inner: CoreEventDetails<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> EventDetails<T> {
|
||||
/// When was the event produced?
|
||||
pub fn phase(&self) -> Phase {
|
||||
self.inner.phase()
|
||||
}
|
||||
|
||||
/// What index is this event in the stored events for this block.
|
||||
pub fn index(&self) -> u32 {
|
||||
self.inner.index()
|
||||
}
|
||||
|
||||
/// The index of the pallet that the event originated from.
|
||||
pub fn pallet_index(&self) -> u8 {
|
||||
self.inner.pallet_index()
|
||||
}
|
||||
|
||||
/// The index of the event variant that the event originated from.
|
||||
pub fn variant_index(&self) -> u8 {
|
||||
self.inner.variant_index()
|
||||
}
|
||||
|
||||
/// The name of the pallet from whence the Event originated.
|
||||
pub fn pallet_name(&self) -> &str {
|
||||
self.inner.pallet_name()
|
||||
}
|
||||
|
||||
/// The name of the event (ie the name of the variant that it corresponds to).
|
||||
pub fn variant_name(&self) -> &str {
|
||||
self.inner.variant_name()
|
||||
}
|
||||
|
||||
/// Fetch details from the metadata for this event.
|
||||
pub fn event_metadata(&self) -> EventMetadataDetails<'_> {
|
||||
self.inner.event_metadata()
|
||||
}
|
||||
|
||||
/// Return _all_ of the bytes representing this event, which include, in order:
|
||||
/// - The phase.
|
||||
/// - Pallet and event index.
|
||||
/// - Event fields.
|
||||
/// - Event Topics.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
self.inner.bytes()
|
||||
}
|
||||
|
||||
/// Return the bytes representing the fields stored in this event.
|
||||
pub fn field_bytes(&self) -> &[u8] {
|
||||
self.inner.field_bytes()
|
||||
}
|
||||
|
||||
/// Decode and provide the event fields back in the form of a [`scale_value::Composite`]
|
||||
/// type which represents the named or unnamed fields that were present in the event.
|
||||
pub fn decode_as_fields<E: DecodeAsFields>(&self) -> Result<E, EventsError> {
|
||||
self.inner.decode_as_fields().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Attempt to decode these [`EventDetails`] into a type representing the event fields.
|
||||
/// Such types are exposed in the codegen as `pallet_name::events::EventName` types.
|
||||
pub fn as_event<E: StaticEvent>(&self) -> Result<Option<E>, EventsError> {
|
||||
self.inner.as_event::<E>().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Attempt to decode these [`EventDetails`] into a root event type (which includes
|
||||
/// the pallet and event enum variants as well as the event fields). A compatible
|
||||
/// type for this is exposed via static codegen as a root level `Event` type.
|
||||
pub fn as_root_event<E: DecodeAsType>(&self) -> Result<E, EventsError> {
|
||||
self.inner.as_root_event::<E>().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Return the topics associated with this event.
|
||||
pub fn topics(&self) -> &[HashFor<T>] {
|
||||
self.inner.topics()
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module exposes the types and such necessary for working with events.
|
||||
//! The two main entry points into events are [`crate::OnlineClient::events()`]
|
||||
//! and calls like [crate::tx::TxProgress::wait_for_finalized_success()].
|
||||
|
||||
mod events_client;
|
||||
mod events_type;
|
||||
|
||||
use crate::client::OnlineClientT;
|
||||
use crate::error::EventsError;
|
||||
use pezkuwi_subxt_core::{
|
||||
Metadata,
|
||||
config::{Config, HashFor},
|
||||
};
|
||||
|
||||
pub use events_client::EventsClient;
|
||||
pub use events_type::{EventDetails, EventMetadataDetails, Events, Phase, StaticEvent};
|
||||
|
||||
/// Creates a new [`Events`] instance by fetching the corresponding bytes at `block_hash` from the client.
|
||||
pub async fn new_events_from_client<T, C>(
|
||||
metadata: Metadata,
|
||||
block_hash: HashFor<T>,
|
||||
client: C,
|
||||
) -> Result<Events<T>, EventsError>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientT<T>,
|
||||
{
|
||||
let event_bytes = events_client::get_event_bytes(client.backend(), block_hash).await?;
|
||||
Ok(Events::<T>::decode_from(event_bytes, metadata))
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Subxt is a library for interacting with Substrate based nodes. Using it looks something like this:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
#![doc = include_str!("../examples/tx_basic.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! Take a look at [the Subxt guide](book) to learn more about how to use Subxt.
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#[cfg(any(
|
||||
all(feature = "web", feature = "native"),
|
||||
not(any(feature = "web", feature = "native"))
|
||||
))]
|
||||
compile_error!("subxt: exactly one of the 'web' and 'native' features should be used.");
|
||||
|
||||
// Internal helper macros
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
// The guide is here.
|
||||
pub mod book;
|
||||
|
||||
// Suppress an unused dependency warning because tokio is
|
||||
// only used in example code snippets at the time of writing.
|
||||
#[cfg(test)]
|
||||
mod only_used_in_docs_or_tests {
|
||||
use pezkuwi_subxt_signer as _;
|
||||
use tokio as _;
|
||||
}
|
||||
|
||||
// Suppress an unused dependency warning because tracing_subscriber is
|
||||
// only used in example code snippets at the time of writing.
|
||||
#[cfg(test)]
|
||||
use tracing_subscriber as _;
|
||||
|
||||
pub mod backend;
|
||||
pub mod blocks;
|
||||
pub mod client;
|
||||
pub mod constants;
|
||||
pub mod custom_values;
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod runtime_api;
|
||||
pub mod storage;
|
||||
pub mod tx;
|
||||
pub mod utils;
|
||||
pub mod view_functions;
|
||||
|
||||
/// This module provides a [`Config`] type, which is used to define various
|
||||
/// types that are important in order to speak to a particular chain.
|
||||
/// [`SubstrateConfig`] provides a default set of these types suitable for the
|
||||
/// default Substrate node implementation, and [`PolkadotConfig`] for a
|
||||
/// Polkadot node.
|
||||
pub mod config {
|
||||
pub use pezkuwi_subxt_core::config::{
|
||||
Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, ExtrinsicParams,
|
||||
ExtrinsicParamsEncoder, Hash, HashFor, Hasher, Header, PolkadotConfig,
|
||||
PolkadotExtrinsicParams, SubstrateConfig, SubstrateExtrinsicParams, TransactionExtension,
|
||||
polkadot, substrate, transaction_extensions,
|
||||
};
|
||||
pub use pezkuwi_subxt_core::error::ExtrinsicParamsError;
|
||||
}
|
||||
|
||||
/// Types representing the metadata obtained from a node.
|
||||
pub mod metadata {
|
||||
pub use pezkuwi_subxt_metadata::*;
|
||||
}
|
||||
|
||||
/// Submit dynamic transactions.
|
||||
pub mod dynamic {
|
||||
pub use pezkuwi_subxt_core::dynamic::*;
|
||||
}
|
||||
|
||||
// Expose light client bits
|
||||
cfg_unstable_light_client! {
|
||||
pub use pezkuwi_subxt_lightclient as lightclient;
|
||||
}
|
||||
|
||||
// Expose a few of the most common types at root,
|
||||
// but leave most types behind their respective modules.
|
||||
pub use crate::{
|
||||
client::{OfflineClient, OnlineClient},
|
||||
config::{Config, PolkadotConfig, SubstrateConfig},
|
||||
error::Error,
|
||||
metadata::Metadata,
|
||||
};
|
||||
|
||||
/// Re-export external crates that are made use of in the subxt API.
|
||||
pub mod ext {
|
||||
pub use codec;
|
||||
pub use frame_metadata;
|
||||
pub use futures;
|
||||
pub use scale_bits;
|
||||
pub use scale_decode;
|
||||
pub use scale_encode;
|
||||
pub use scale_value;
|
||||
pub use pezkuwi_subxt_core;
|
||||
pub use pezkuwi_subxt_rpcs;
|
||||
|
||||
cfg_jsonrpsee! {
|
||||
pub use jsonrpsee;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a strongly typed API for interacting with a Substrate runtime from its metadata of WASM.
|
||||
///
|
||||
/// # Metadata
|
||||
///
|
||||
/// First, you'll need to get hold of some metadata for the node you'd like to interact with. One
|
||||
/// way to do this is by using the `subxt` CLI tool:
|
||||
///
|
||||
/// ```bash
|
||||
/// # Install the CLI tool:
|
||||
/// cargo install subxt-cli
|
||||
/// # Use it to download metadata (in this case, from a node running locally)
|
||||
/// subxt metadata > polkadot_metadata.scale
|
||||
/// ```
|
||||
///
|
||||
/// Run `subxt metadata --help` for more options.
|
||||
///
|
||||
/// # Basic usage
|
||||
///
|
||||
/// We can generate an interface to a chain given either:
|
||||
/// - A locally saved SCALE encoded metadata file (see above) for that chain,
|
||||
/// - The Runtime WASM for that chain, or
|
||||
/// - A URL pointing at the JSON-RPC interface for a node on that chain.
|
||||
///
|
||||
/// In each case, the `subxt` macro will use this data to populate the annotated module with all of the methods
|
||||
/// and types required for interacting with the chain that the Runtime/metadata was loaded from.
|
||||
///
|
||||
/// Let's look at each of these:
|
||||
///
|
||||
/// ## Using a locally saved metadata file
|
||||
///
|
||||
/// Annotate a Rust module with the `subxt` attribute referencing a metadata file like so:
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// You can use the `$OUT_DIR` placeholder in the path to reference metadata generated at build time:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "$OUT_DIR/metadata.scale",
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Using a WASM runtime via `runtime_path = "..."`
|
||||
///
|
||||
/// This requires the `runtime-wasm-path` feature flag.
|
||||
///
|
||||
/// Annotate a Rust module with the `subxt` attribute referencing some runtime WASM like so:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_path = "../artifacts/westend_runtime.wasm",
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// You can also use the `$OUT_DIR` placeholder in the path to reference WASM files generated at build time:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_path = "$OUT_DIR/runtime.wasm",
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Connecting to a node to download metadata via `runtime_metadata_insecure_url = "..."`
|
||||
///
|
||||
/// This will, at compile time, connect to the JSON-RPC interface for some node at the URL given,
|
||||
/// download the metadata from it, and use that. This can be useful in CI, but is **not recommended**
|
||||
/// in production code, because:
|
||||
///
|
||||
/// - The compilation time is increased since we have to download metadata from a URL each time. If
|
||||
/// the node we connect to is unresponsive, this will be slow or could fail.
|
||||
/// - The metadata may change from what is expected without notice, causing compilation to fail if
|
||||
/// it leads to changes in the generated interfaces that are being used.
|
||||
/// - The node that you connect to could be malicious and provide incorrect metadata for the chain.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443"
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// # Configuration
|
||||
///
|
||||
/// This macro supports a number of attributes to configure what is generated:
|
||||
///
|
||||
/// ## `crate = "..."`
|
||||
///
|
||||
/// Use this attribute to specify a custom path to the `pezkuwi_subxt_core` crate:
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// # pub extern crate pezkuwi_subxt_core;
|
||||
/// # pub mod path { pub mod to { pub use pezkuwi_subxt_core; } }
|
||||
/// # fn main() {}
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
/// crate = "crate::path::to::pezkuwi_subxt_core"
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// This is useful if you write a library which uses this macro, but don't want to force users to depend on `subxt`
|
||||
/// at the top level too. By default the path `::subxt` is used.
|
||||
///
|
||||
/// ## `substitute_type(path = "...", with = "...")`
|
||||
///
|
||||
/// This attribute replaces any reference to the generated type at the path given by `path` with a
|
||||
/// reference to the path given by `with`.
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
/// substitute_type(path = "sp_arithmetic::per_things::Perbill", with = "crate::Foo")
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
///
|
||||
/// # #[derive(
|
||||
/// # scale_encode::EncodeAsType,
|
||||
/// # scale_decode::DecodeAsType,
|
||||
/// # codec::Encode,
|
||||
/// # codec::Decode,
|
||||
/// # Clone,
|
||||
/// # Debug,
|
||||
/// # )]
|
||||
/// // In reality this needs some traits implementing on
|
||||
/// // it to allow it to be used in place of Perbill:
|
||||
/// pub struct Foo(u32);
|
||||
/// # impl codec::CompactAs for Foo {
|
||||
/// # type As = u32;
|
||||
/// # fn encode_as(&self) -> &Self::As {
|
||||
/// # &self.0
|
||||
/// # }
|
||||
/// # fn decode_from(x: Self::As) -> Result<Self, codec::Error> {
|
||||
/// # Ok(Foo(x))
|
||||
/// # }
|
||||
/// # }
|
||||
/// # impl From<codec::Compact<Foo>> for Foo {
|
||||
/// # fn from(v: codec::Compact<Foo>) -> Foo {
|
||||
/// # v.0
|
||||
/// # }
|
||||
/// # }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// If the type you're substituting contains generic parameters, you can "pattern match" on those, and
|
||||
/// make use of them in the substituted type, like so:
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
/// substitute_type(
|
||||
/// path = "sp_runtime::multiaddress::MultiAddress<A, B>",
|
||||
/// with = "::pezkuwi_subxt::utils::Static<sp_runtime::MultiAddress<A, B>>"
|
||||
/// )
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// The above is also an example of using the [`crate::utils::Static`] type to wrap some type which doesn't
|
||||
/// on it's own implement [`scale_encode::EncodeAsType`] or [`scale_decode::DecodeAsType`], which are required traits
|
||||
/// for any substitute type to implement by default.
|
||||
///
|
||||
/// ## `derive_for_all_types = "..."`
|
||||
///
|
||||
/// By default, all generated types derive a small set of traits. This attribute allows you to derive additional
|
||||
/// traits on all generated types:
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
/// derive_for_all_types = "Eq, PartialEq"
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// Any substituted types (including the default substitutes) must also implement these traits in order to avoid errors
|
||||
/// here.
|
||||
///
|
||||
/// ## `derive_for_type(path = "...", derive = "...")`
|
||||
///
|
||||
/// Unlike the above, which derives some trait on every generated type, this attribute allows you to derive traits only
|
||||
/// for specific types. Note that any types which are used inside the specified type may also need to derive the same traits.
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
/// derive_for_all_types = "Eq, PartialEq",
|
||||
/// derive_for_type(path = "frame_support::PalletId", derive = "Ord, PartialOrd"),
|
||||
/// derive_for_type(path = "sp_runtime::ModuleError", derive = "Hash"),
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// ## `generate_docs`
|
||||
///
|
||||
/// By default, documentation is not generated via the macro, since IDEs do not typically make use of it. This attribute
|
||||
/// forces documentation to be generated, too.
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
/// generate_docs
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// ## `runtime_types_only`
|
||||
///
|
||||
/// By default, the macro will generate various interfaces to make using Subxt simpler in addition with any types that need
|
||||
/// generating to make this possible. This attribute makes the codegen only generate the types and not the Subxt interface.
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
/// runtime_types_only
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// ## `no_default_derives`
|
||||
///
|
||||
/// By default, the macro will add all derives necessary for the generated code to play nicely with Subxt. Adding this attribute
|
||||
/// removes all default derives.
|
||||
///
|
||||
/// ```rust,no_run,standalone_crate
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
|
||||
/// runtime_types_only,
|
||||
/// no_default_derives,
|
||||
/// derive_for_all_types="codec::Encode, codec::Decode"
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
///
|
||||
/// **Note**: At the moment, you must derive at least one of `codec::Encode` or `codec::Decode` or `scale_encode::EncodeAsType` or
|
||||
/// `scale_decode::DecodeAsType` (because we add `#[codec(..)]` attributes on some fields/types during codegen), and you must use this
|
||||
/// feature in conjunction with `runtime_types_only` (or manually specify a bunch of defaults to make codegen work properly when
|
||||
/// generating the subxt interfaces).
|
||||
///
|
||||
/// ## `unstable_metadata`
|
||||
///
|
||||
/// This attribute works only in combination with `runtime_metadata_insecure_url`. By default, the macro will fetch the latest stable
|
||||
/// version of the metadata from the target node. This attribute makes the codegen attempt to fetch the unstable version of
|
||||
/// the metadata first. This is **not recommended** in production code, since the unstable metadata a node is providing is likely
|
||||
/// to be incompatible with Subxt.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[subxt::subxt(
|
||||
/// runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443",
|
||||
/// unstable_metadata
|
||||
/// )]
|
||||
/// mod polkadot {}
|
||||
/// ```
|
||||
pub use pezkuwi_subxt_macro::subxt;
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
macro_rules! cfg_feature {
|
||||
($feature:literal, $($item:item)*) => {
|
||||
$(
|
||||
#[cfg(feature = $feature)]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = $feature)))]
|
||||
$item
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! cfg_unstable_light_client {
|
||||
($($item:item)*) => {
|
||||
crate::macros::cfg_feature!("unstable-light-client", $($item)*);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! cfg_reconnecting_rpc_client {
|
||||
($($item:item)*) => {
|
||||
crate::macros::cfg_feature!("reconnecting-rpc-client", $($item)*);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! cfg_jsonrpsee {
|
||||
($($item:item)*) => {
|
||||
crate::macros::cfg_feature!("jsonrpsee", $($item)*);
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
macro_rules! cfg_jsonrpsee_native {
|
||||
($($item:item)*) => {
|
||||
$(
|
||||
#[cfg(all(feature = "jsonrpsee", feature = "native"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(all(feature = "jsonrpsee", feature = "native"))))]
|
||||
$item
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
macro_rules! cfg_jsonrpsee_web {
|
||||
($($item:item)*) => {
|
||||
$(
|
||||
#[cfg(all(feature = "jsonrpsee", feature = "web"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(all(feature = "jsonrpsee", feature = "web"))))]
|
||||
$item
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) use {cfg_feature, cfg_jsonrpsee, cfg_unstable_light_client};
|
||||
|
||||
// Only used by light-client.
|
||||
#[allow(unused)]
|
||||
pub(crate) use {cfg_jsonrpsee_native, cfg_jsonrpsee_web, cfg_reconnecting_rpc_client};
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Types associated with executing runtime API calls.
|
||||
|
||||
mod runtime_client;
|
||||
mod runtime_types;
|
||||
|
||||
pub use runtime_client::RuntimeApiClient;
|
||||
pub use runtime_types::RuntimeApi;
|
||||
pub use pezkuwi_subxt_core::runtime_api::payload::{DynamicPayload, Payload, StaticPayload, dynamic};
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::runtime_types::RuntimeApi;
|
||||
|
||||
use crate::{
|
||||
backend::BlockRef,
|
||||
client::OnlineClientT,
|
||||
config::{Config, HashFor},
|
||||
error::RuntimeApiError,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
|
||||
/// Execute runtime API calls.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct RuntimeApiClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> RuntimeApiClient<T, Client> {
|
||||
/// Create a new [`RuntimeApiClient`]
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> RuntimeApiClient<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Obtain a runtime API interface at some block hash.
|
||||
pub fn at(&self, block_ref: impl Into<BlockRef<HashFor<T>>>) -> RuntimeApi<T, Client> {
|
||||
RuntimeApi::new(self.client.clone(), block_ref.into())
|
||||
}
|
||||
|
||||
/// Obtain a runtime API interface at the latest finalized block.
|
||||
pub fn at_latest(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<RuntimeApi<T, Client>, RuntimeApiError>> + Send + 'static {
|
||||
// Clone and pass the client in like this so that we can explicitly
|
||||
// return a Future that's Send + 'static, rather than tied to &self.
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
// get the ref for the latest finalized block and use that.
|
||||
let block_ref = client
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(RuntimeApiError::CannotGetLatestFinalizedBlock)?;
|
||||
|
||||
Ok(RuntimeApi::new(client, block_ref))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::Payload;
|
||||
use crate::{
|
||||
backend::BlockRef,
|
||||
client::OnlineClientT,
|
||||
config::{Config, HashFor},
|
||||
error::RuntimeApiError,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
|
||||
/// Execute runtime API calls.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct RuntimeApi<T: Config, Client> {
|
||||
client: Client,
|
||||
block_ref: BlockRef<HashFor<T>>,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config, Client> RuntimeApi<T, Client> {
|
||||
/// Create a new [`RuntimeApi`]
|
||||
pub(crate) fn new(client: Client, block_ref: BlockRef<HashFor<T>>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
block_ref,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> RuntimeApi<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Run the validation logic against some runtime API payload you'd like to use. Returns `Ok(())`
|
||||
/// if the payload is valid (or if it's not possible to check since the payload has no validation hash).
|
||||
/// Return an error if the payload was not valid or something went wrong trying to validate it (ie
|
||||
/// the runtime API in question do not exist at all)
|
||||
pub fn validate<Call: Payload>(&self, payload: Call) -> Result<(), RuntimeApiError> {
|
||||
pezkuwi_subxt_core::runtime_api::validate(payload, &self.client.metadata()).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Execute a raw runtime API call. This returns the raw bytes representing the result
|
||||
/// of this call. The caller is responsible for decoding the result.
|
||||
pub fn call_raw<'a>(
|
||||
&self,
|
||||
function: &'a str,
|
||||
call_parameters: Option<&'a [u8]>,
|
||||
) -> impl Future<Output = Result<Vec<u8>, RuntimeApiError>> + use<'a, Client, T> {
|
||||
let client = self.client.clone();
|
||||
let block_hash = self.block_ref.hash();
|
||||
// Ensure that the returned future doesn't have a lifetime tied to api.runtime_api(),
|
||||
// which is a temporary thing we'll be throwing away quickly:
|
||||
async move {
|
||||
let data = client
|
||||
.backend()
|
||||
.call(function, call_parameters, block_hash)
|
||||
.await
|
||||
.map_err(RuntimeApiError::CannotCallApi)?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a runtime API call.
|
||||
pub fn call<Call: Payload>(
|
||||
&self,
|
||||
payload: Call,
|
||||
) -> impl Future<Output = Result<Call::ReturnType, RuntimeApiError>> + use<Call, Client, T>
|
||||
{
|
||||
let client = self.client.clone();
|
||||
let block_hash = self.block_ref.hash();
|
||||
// Ensure that the returned future doesn't have a lifetime tied to api.runtime_api(),
|
||||
// which is a temporary thing we'll be throwing away quickly:
|
||||
async move {
|
||||
let metadata = client.metadata();
|
||||
|
||||
// Validate the runtime API payload hash against the compile hash from codegen.
|
||||
pezkuwi_subxt_core::runtime_api::validate(&payload, &metadata)?;
|
||||
|
||||
// Encode the arguments of the runtime call.
|
||||
let call_name = pezkuwi_subxt_core::runtime_api::call_name(&payload);
|
||||
let call_args = pezkuwi_subxt_core::runtime_api::call_args(&payload, &metadata)?;
|
||||
|
||||
// Make the call.
|
||||
let bytes = client
|
||||
.backend()
|
||||
.call(&call_name, Some(call_args.as_slice()), block_hash)
|
||||
.await
|
||||
.map_err(RuntimeApiError::CannotCallApi)?;
|
||||
|
||||
// Decode the response.
|
||||
let value = pezkuwi_subxt_core::runtime_api::decode_value(&mut &*bytes, &payload, &metadata)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Types associated with accessing and working with storage items.
|
||||
|
||||
mod storage_client;
|
||||
mod storage_client_at;
|
||||
|
||||
pub use storage_client::StorageClient;
|
||||
pub use storage_client_at::{StorageClientAt, StorageEntryClient, StorageKeyValue, StorageValue};
|
||||
pub use pezkuwi_subxt_core::storage::address::{Address, DynamicAddress, StaticAddress, dynamic};
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::storage_client_at::StorageClientAt;
|
||||
use crate::{
|
||||
backend::BlockRef,
|
||||
client::{OfflineClientT, OnlineClientT},
|
||||
config::{Config, HashFor},
|
||||
error::StorageError,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
use pezkuwi_subxt_core::storage::address::Address;
|
||||
|
||||
/// Query the runtime storage.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct StorageClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> StorageClient<T, Client> {
|
||||
/// Create a new [`StorageClient`]
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> StorageClient<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OfflineClientT<T>,
|
||||
{
|
||||
/// Run the validation logic against some storage address you'd like to access. Returns `Ok(())`
|
||||
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
|
||||
/// Return an error if the address was not valid or something went wrong trying to validate it (ie
|
||||
/// the pallet or storage entry in question do not exist at all).
|
||||
pub fn validate<Addr: Address>(&self, address: &Addr) -> Result<(), StorageError> {
|
||||
pezkuwi_subxt_core::storage::validate(address, &self.client.metadata()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> StorageClient<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Obtain storage at some block hash.
|
||||
pub fn at(&self, block_ref: impl Into<BlockRef<HashFor<T>>>) -> StorageClientAt<T, Client> {
|
||||
StorageClientAt::new(self.client.clone(), block_ref.into())
|
||||
}
|
||||
|
||||
/// Obtain storage at the latest finalized block.
|
||||
pub fn at_latest(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<StorageClientAt<T, Client>, StorageError>> + Send + 'static
|
||||
{
|
||||
// Clone and pass the client in like this so that we can explicitly
|
||||
// return a Future that's Send + 'static, rather than tied to &self.
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
// get the ref for the latest finalized block and use that.
|
||||
let block_ref = client
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(StorageError::CannotGetLatestFinalizedBlock)?;
|
||||
|
||||
Ok(StorageClientAt::new(client, block_ref))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
// Copyright 2019-2025 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::{BackendExt, BlockRef},
|
||||
client::{OfflineClientT, OnlineClientT},
|
||||
config::{Config, HashFor},
|
||||
error::StorageError,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use futures::StreamExt;
|
||||
use std::marker::PhantomData;
|
||||
use pezkuwi_subxt_core::Metadata;
|
||||
use pezkuwi_subxt_core::storage::{PrefixOf, address::Address};
|
||||
use pezkuwi_subxt_core::utils::{Maybe, Yes};
|
||||
|
||||
pub use pezkuwi_subxt_core::storage::{StorageKeyValue, StorageValue};
|
||||
|
||||
/// Query the runtime storage.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct StorageClientAt<T: Config, Client> {
|
||||
client: Client,
|
||||
metadata: Metadata,
|
||||
block_ref: BlockRef<HashFor<T>>,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> StorageClientAt<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OfflineClientT<T>,
|
||||
{
|
||||
/// Create a new [`StorageClientAt`].
|
||||
pub(crate) fn new(client: Client, block_ref: BlockRef<HashFor<T>>) -> Self {
|
||||
// Retrieve and store metadata here so that we can borrow it in
|
||||
// subsequent structs, and thus also borrow storage info and
|
||||
// things that borrow from metadata.
|
||||
let metadata = client.metadata();
|
||||
|
||||
Self {
|
||||
client,
|
||||
metadata,
|
||||
block_ref,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> StorageClientAt<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OfflineClientT<T>,
|
||||
{
|
||||
/// This returns a [`StorageEntryClient`], which allows working with the storage entry at the provided address.
|
||||
pub fn entry<Addr: Address>(
|
||||
&self,
|
||||
address: Addr,
|
||||
) -> Result<StorageEntryClient<'_, T, Client, Addr, Addr::IsPlain>, StorageError> {
|
||||
let inner = pezkuwi_subxt_core::storage::entry(address, &self.metadata)?;
|
||||
Ok(StorageEntryClient {
|
||||
inner,
|
||||
client: self.client.clone(),
|
||||
block_ref: self.block_ref.clone(),
|
||||
_marker: core::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> StorageClientAt<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// This is essentially a shorthand for `client.entry(addr)?.fetch(key_parts)`. See [`StorageEntryClient::fetch()`].
|
||||
pub async fn fetch<Addr: Address>(
|
||||
&self,
|
||||
addr: Addr,
|
||||
key_parts: Addr::KeyParts,
|
||||
) -> Result<StorageValue<'_, Addr::Value>, StorageError> {
|
||||
let entry = pezkuwi_subxt_core::storage::entry(addr, &self.metadata)?;
|
||||
fetch(&entry, &self.client, self.block_ref.hash(), key_parts).await
|
||||
}
|
||||
|
||||
/// This is essentially a shorthand for `client.entry(addr)?.try_fetch(key_parts)`. See [`StorageEntryClient::try_fetch()`].
|
||||
pub async fn try_fetch<Addr: Address>(
|
||||
&self,
|
||||
addr: Addr,
|
||||
key_parts: Addr::KeyParts,
|
||||
) -> Result<Option<StorageValue<'_, Addr::Value>>, StorageError> {
|
||||
let entry = pezkuwi_subxt_core::storage::entry(addr, &self.metadata)?;
|
||||
try_fetch(&entry, &self.client, self.block_ref.hash(), key_parts).await
|
||||
}
|
||||
|
||||
/// This is essentially a shorthand for `client.entry(addr)?.iter(key_parts)`. See [`StorageEntryClient::iter()`].
|
||||
pub async fn iter<Addr: Address, KeyParts: PrefixOf<Addr::KeyParts>>(
|
||||
&'_ self,
|
||||
addr: Addr,
|
||||
key_parts: KeyParts,
|
||||
) -> Result<
|
||||
impl futures::Stream<Item = Result<StorageKeyValue<'_, Addr>, StorageError>>
|
||||
+ use<'_, Addr, Client, T, KeyParts>,
|
||||
StorageError,
|
||||
> {
|
||||
let entry = pezkuwi_subxt_core::storage::entry(addr, &self.metadata)?;
|
||||
iter(entry, &self.client, self.block_ref.hash(), key_parts).await
|
||||
}
|
||||
|
||||
/// In rare cases, you may wish to fetch a storage value that does not live at a typical address. This method
|
||||
/// is a fallback for those cases, and allows you to provide the raw storage key bytes corresponding to the
|
||||
/// entry you wish to obtain. The response will either be the bytes for the value found at that location, or
|
||||
/// otherwise an error. [`StorageError::NoValueFound`] will be returned in the event that the request was valid
|
||||
/// but no value lives at the given location).
|
||||
pub async fn fetch_raw(&self, key_bytes: Vec<u8>) -> Result<Vec<u8>, StorageError> {
|
||||
let block_hash = self.block_ref.hash();
|
||||
let value = self
|
||||
.client
|
||||
.backend()
|
||||
.storage_fetch_value(key_bytes, block_hash)
|
||||
.await
|
||||
.map_err(StorageError::CannotFetchValue)?
|
||||
.ok_or(StorageError::NoValueFound)?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// The storage version of a pallet.
|
||||
/// The storage version refers to the `frame_support::traits::Metadata::StorageVersion` type.
|
||||
pub async fn storage_version(&self, pallet_name: impl AsRef<str>) -> Result<u16, StorageError> {
|
||||
// construct the storage key. This is done similarly in
|
||||
// `frame_support::traits::metadata::StorageVersion::storage_key()`:
|
||||
let mut key_bytes: Vec<u8> = vec![];
|
||||
key_bytes.extend(&pezsp_crypto_hashing::twox_128(
|
||||
pallet_name.as_ref().as_bytes(),
|
||||
));
|
||||
key_bytes.extend(&pezsp_crypto_hashing::twox_128(b":__STORAGE_VERSION__:"));
|
||||
|
||||
// fetch the raw bytes and decode them into the StorageVersion struct:
|
||||
let storage_version_bytes = self.fetch_raw(key_bytes).await?;
|
||||
|
||||
<u16 as codec::Decode>::decode(&mut &storage_version_bytes[..])
|
||||
.map_err(StorageError::CannotDecodeStorageVersion)
|
||||
}
|
||||
|
||||
/// Fetch the runtime WASM code.
|
||||
pub async fn runtime_wasm_code(&self) -> Result<Vec<u8>, StorageError> {
|
||||
// note: this should match the `CODE` constant in `sp_core::storage::well_known_keys`
|
||||
self.fetch_raw(b":code".to_vec()).await
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a single storage entry (be it a plain value or map)
|
||||
/// and the operations that can be performed on it.
|
||||
pub struct StorageEntryClient<'atblock, T: Config, Client, Addr, IsPlain> {
|
||||
inner: pezkuwi_subxt_core::storage::StorageEntry<'atblock, Addr>,
|
||||
client: Client,
|
||||
block_ref: BlockRef<HashFor<T>>,
|
||||
_marker: PhantomData<(T, IsPlain)>,
|
||||
}
|
||||
|
||||
impl<'atblock, T, Client, Addr, IsPlain> StorageEntryClient<'atblock, T, Client, Addr, IsPlain>
|
||||
where
|
||||
T: Config,
|
||||
Addr: Address,
|
||||
{
|
||||
/// Name of the pallet containing this storage entry.
|
||||
pub fn pallet_name(&self) -> &str {
|
||||
self.inner.pallet_name()
|
||||
}
|
||||
|
||||
/// Name of the storage entry.
|
||||
pub fn entry_name(&self) -> &str {
|
||||
self.inner.entry_name()
|
||||
}
|
||||
|
||||
/// Is the storage entry a plain value?
|
||||
pub fn is_plain(&self) -> bool {
|
||||
self.inner.is_plain()
|
||||
}
|
||||
|
||||
/// Is the storage entry a map?
|
||||
pub fn is_map(&self) -> bool {
|
||||
self.inner.is_map()
|
||||
}
|
||||
|
||||
/// Return the default value for this storage entry, if there is one. Returns `None` if there
|
||||
/// is no default value.
|
||||
pub fn default_value(&self) -> Option<StorageValue<'atblock, Addr::Value>> {
|
||||
self.inner.default_value()
|
||||
}
|
||||
}
|
||||
|
||||
// Plain values get a fetch method with no extra arguments.
|
||||
impl<'atblock, T, Client, Addr> StorageEntryClient<'atblock, T, Client, Addr, Yes>
|
||||
where
|
||||
T: Config,
|
||||
Addr: Address,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Fetch the storage value at this location. If no value is found, the default value will be returned
|
||||
/// for this entry if one exists. If no value is found and no default value exists, an error will be returned.
|
||||
pub async fn fetch(&self) -> Result<StorageValue<'atblock, Addr::Value>, StorageError> {
|
||||
let value = self.try_fetch().await?.map_or_else(
|
||||
|| self.inner.default_value().ok_or(StorageError::NoValueFound),
|
||||
Ok,
|
||||
)?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Fetch the storage value at this location. If no value is found, `None` will be returned.
|
||||
pub async fn try_fetch(
|
||||
&self,
|
||||
) -> Result<Option<StorageValue<'atblock, Addr::Value>>, StorageError> {
|
||||
let value = self
|
||||
.client
|
||||
.backend()
|
||||
.storage_fetch_value(self.key_prefix().to_vec(), self.block_ref.hash())
|
||||
.await
|
||||
.map_err(StorageError::CannotFetchValue)?
|
||||
.map(|bytes| self.inner.value(bytes));
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// This is identical to [`StorageEntryClient::key_prefix()`] and is the full
|
||||
/// key for this storage entry.
|
||||
pub fn key(&self) -> [u8; 32] {
|
||||
self.inner.key_prefix()
|
||||
}
|
||||
|
||||
/// The keys for plain storage values are always 32 byte hashes.
|
||||
pub fn key_prefix(&self) -> [u8; 32] {
|
||||
self.inner.key_prefix()
|
||||
}
|
||||
}
|
||||
|
||||
// When HasDefaultValue = Yes, we expect there to exist a valid default value and will use that
|
||||
// if we fetch an entry and get nothing back.
|
||||
impl<'atblock, T, Client, Addr> StorageEntryClient<'atblock, T, Client, Addr, Maybe>
|
||||
where
|
||||
T: Config,
|
||||
Addr: Address,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Fetch a storage value within this storage entry.
|
||||
///
|
||||
/// This entry may be a map, and so you must provide the relevant values for each part of the storage
|
||||
/// key that is required in order to point to a single value.
|
||||
///
|
||||
/// If no value is found, the default value will be returned for this entry if one exists. If no value is
|
||||
/// found and no default value exists, an error will be returned.
|
||||
pub async fn fetch(
|
||||
&self,
|
||||
key_parts: Addr::KeyParts,
|
||||
) -> Result<StorageValue<'atblock, Addr::Value>, StorageError> {
|
||||
fetch(&self.inner, &self.client, self.block_ref.hash(), key_parts).await
|
||||
}
|
||||
|
||||
/// Fetch a storage value within this storage entry.
|
||||
///
|
||||
/// This entry may be a map, and so you must provide the relevant values for each part of the storage
|
||||
/// key that is required in order to point to a single value.
|
||||
///
|
||||
/// If no value is found, `None` will be returned.
|
||||
pub async fn try_fetch(
|
||||
&self,
|
||||
key_parts: Addr::KeyParts,
|
||||
) -> Result<Option<StorageValue<'atblock, Addr::Value>>, StorageError> {
|
||||
try_fetch(&self.inner, &self.client, self.block_ref.hash(), key_parts).await
|
||||
}
|
||||
|
||||
/// Iterate over storage values within this storage entry.
|
||||
///
|
||||
/// You may provide any prefix of the values needed to point to a single value. Normally you will
|
||||
/// provide `()` to iterate over _everything_, or `(first_key,)` to iterate over everything underneath
|
||||
/// `first_key` in the map, or `(first_key, second_key)` to iterate over everything underneath `first_key`
|
||||
/// and `second_key` in the map, and so on, up to the actual depth of the map - 1.
|
||||
pub async fn iter<KeyParts: PrefixOf<Addr::KeyParts>>(
|
||||
&self,
|
||||
key_parts: KeyParts,
|
||||
) -> Result<
|
||||
impl futures::Stream<Item = Result<StorageKeyValue<'atblock, Addr>, StorageError>>
|
||||
+ use<'atblock, Addr, Client, T, KeyParts>,
|
||||
StorageError,
|
||||
> {
|
||||
iter(
|
||||
self.inner.clone(),
|
||||
&self.client,
|
||||
self.block_ref.hash(),
|
||||
key_parts,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// This returns a full key to a single value in this storage entry.
|
||||
pub fn key(&self, key_parts: Addr::KeyParts) -> Result<Vec<u8>, StorageError> {
|
||||
let key = self.inner.fetch_key(key_parts)?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// This returns valid keys to iterate over the storage entry at the available levels.
|
||||
pub fn iter_key<KeyParts: PrefixOf<Addr::KeyParts>>(
|
||||
&self,
|
||||
key_parts: KeyParts,
|
||||
) -> Result<Vec<u8>, StorageError> {
|
||||
let key = self.inner.iter_key(key_parts)?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// The first 32 bytes of the storage entry key, which points to the entry but not necessarily
|
||||
/// a single storage value (unless the entry is a plain value).
|
||||
pub fn key_prefix(&self) -> [u8; 32] {
|
||||
self.inner.key_prefix()
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch<'atblock, T: Config, Client: OnlineClientT<T>, Addr: Address>(
|
||||
entry: &pezkuwi_subxt_core::storage::StorageEntry<'atblock, Addr>,
|
||||
client: &Client,
|
||||
block_hash: HashFor<T>,
|
||||
key_parts: Addr::KeyParts,
|
||||
) -> Result<StorageValue<'atblock, Addr::Value>, StorageError> {
|
||||
let value = try_fetch(entry, client, block_hash, key_parts)
|
||||
.await?
|
||||
.or_else(|| entry.default_value())
|
||||
.unwrap();
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
async fn try_fetch<'atblock, T: Config, Client: OnlineClientT<T>, Addr: Address>(
|
||||
entry: &pezkuwi_subxt_core::storage::StorageEntry<'atblock, Addr>,
|
||||
client: &Client,
|
||||
block_hash: HashFor<T>,
|
||||
key_parts: Addr::KeyParts,
|
||||
) -> Result<Option<StorageValue<'atblock, Addr::Value>>, StorageError> {
|
||||
let key = entry.fetch_key(key_parts)?;
|
||||
|
||||
let value = client
|
||||
.backend()
|
||||
.storage_fetch_value(key, block_hash)
|
||||
.await
|
||||
.map_err(StorageError::CannotFetchValue)?
|
||||
.map(|bytes| entry.value(bytes))
|
||||
.or_else(|| entry.default_value());
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
async fn iter<
|
||||
'atblock,
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
Addr: Address,
|
||||
KeyParts: PrefixOf<Addr::KeyParts>,
|
||||
>(
|
||||
entry: pezkuwi_subxt_core::storage::StorageEntry<'atblock, Addr>,
|
||||
client: &Client,
|
||||
block_hash: HashFor<T>,
|
||||
key_parts: KeyParts,
|
||||
) -> Result<
|
||||
impl futures::Stream<Item = Result<StorageKeyValue<'atblock, Addr>, StorageError>>
|
||||
+ use<'atblock, Addr, Client, T, KeyParts>,
|
||||
StorageError,
|
||||
> {
|
||||
let key_bytes = entry.iter_key(key_parts)?;
|
||||
|
||||
let stream = client
|
||||
.backend()
|
||||
.storage_fetch_descendant_values(key_bytes, block_hash)
|
||||
.await
|
||||
.map_err(StorageError::CannotIterateValues)?
|
||||
.map(move |kv| {
|
||||
let kv = match kv {
|
||||
Ok(kv) => kv,
|
||||
Err(e) => return Err(StorageError::StreamFailure(e)),
|
||||
};
|
||||
Ok(entry.key_value(kv.key, kv.value))
|
||||
});
|
||||
|
||||
Ok(Box::pin(stream))
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Create and submit extrinsics.
|
||||
//!
|
||||
//! An extrinsic is submitted with an "signed extra" and "additional" parameters, which can be
|
||||
//! different for each chain. The trait [`crate::config::ExtrinsicParams`] determines exactly which
|
||||
//! additional and signed extra parameters are used when constructing an extrinsic, and is a part
|
||||
//! of the chain configuration (see [`crate::config::Config`]).
|
||||
|
||||
mod tx_client;
|
||||
mod tx_progress;
|
||||
|
||||
pub use pezkuwi_subxt_core::tx::payload::{DefaultPayload, DynamicPayload, Payload, dynamic};
|
||||
pub use pezkuwi_subxt_core::tx::signer::{self, Signer};
|
||||
pub use tx_client::{
|
||||
DefaultParams, PartialTransaction, SubmittableTransaction, TransactionInvalid,
|
||||
TransactionUnknown, TxClient, ValidationResult,
|
||||
};
|
||||
pub use tx_progress::{TxInBlock, TxProgress, TxStatus};
|
||||
+997
@@ -0,0 +1,997 @@
|
||||
// Copyright 2019-2025 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::{BackendExt, BlockRef, TransactionStatus},
|
||||
client::{OfflineClientT, OnlineClientT},
|
||||
config::{Config, ExtrinsicParams, HashFor, Header},
|
||||
error::{ExtrinsicError, TransactionStatusError},
|
||||
tx::{Payload, Signer as SignerT, TxProgress},
|
||||
utils::PhantomDataSendSync,
|
||||
};
|
||||
use codec::{Compact, Decode, Encode};
|
||||
use derive_where::derive_where;
|
||||
use futures::future::{TryFutureExt, try_join};
|
||||
use pezkuwi_subxt_core::tx::TransactionVersion;
|
||||
|
||||
/// A client for working with transactions.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct TxClient<T: Config, Client> {
|
||||
client: Client,
|
||||
_marker: PhantomDataSendSync<T>,
|
||||
}
|
||||
|
||||
impl<T: Config, Client> TxClient<T, Client> {
|
||||
/// Create a new [`TxClient`]
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: PhantomDataSendSync::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, C: OfflineClientT<T>> TxClient<T, C> {
|
||||
/// Run the validation logic against some transaction you'd like to submit. Returns `Ok(())`
|
||||
/// if the call is valid (or if it's not possible to check since the call has no validation hash).
|
||||
/// Return an error if the call was not valid or something went wrong trying to validate it (ie
|
||||
/// the pallet or call in question do not exist at all).
|
||||
pub fn validate<Call>(&self, call: &Call) -> Result<(), ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
pezkuwi_subxt_core::tx::validate(call, &self.client.metadata()).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Return the SCALE encoded bytes representing the call data of the transaction.
|
||||
pub fn call_data<Call>(&self, call: &Call) -> Result<Vec<u8>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
pezkuwi_subxt_core::tx::call_data(call, &self.client.metadata()).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Creates an unsigned transaction without submitting it. Depending on the metadata, we might end
|
||||
/// up constructing either a v4 or v5 transaction. See [`Self::create_v4_unsigned`] or
|
||||
/// [`Self::create_v5_bare`] if you'd like to explicitly create an unsigned transaction of a certain version.
|
||||
pub fn create_unsigned<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
let metadata = self.client.metadata();
|
||||
let tx = match pezkuwi_subxt_core::tx::suggested_version(&metadata)? {
|
||||
TransactionVersion::V4 => pezkuwi_subxt_core::tx::create_v4_unsigned(call, &metadata),
|
||||
TransactionVersion::V5 => pezkuwi_subxt_core::tx::create_v5_bare(call, &metadata),
|
||||
}?;
|
||||
|
||||
Ok(SubmittableTransaction {
|
||||
client: self.client.clone(),
|
||||
inner: tx,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a v4 unsigned (no signature or transaction extensions) transaction without submitting it.
|
||||
///
|
||||
/// Prefer [`Self::create_unsigned()`] if you don't know which version to create; this will pick the
|
||||
/// most suitable one for the given chain.
|
||||
pub fn create_v4_unsigned<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
let metadata = self.client.metadata();
|
||||
let tx = pezkuwi_subxt_core::tx::create_v4_unsigned(call, &metadata)?;
|
||||
|
||||
Ok(SubmittableTransaction {
|
||||
client: self.client.clone(),
|
||||
inner: tx,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a v5 "bare" (no signature or transaction extensions) transaction without submitting it.
|
||||
///
|
||||
/// Prefer [`Self::create_unsigned()`] if you don't know which version to create; this will pick the
|
||||
/// most suitable one for the given chain.
|
||||
pub fn create_v5_bare<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
let metadata = self.client.metadata();
|
||||
let tx = pezkuwi_subxt_core::tx::create_v5_bare(call, &metadata)?;
|
||||
|
||||
Ok(SubmittableTransaction {
|
||||
client: self.client.clone(),
|
||||
inner: tx,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a partial transaction. Depending on the metadata, we might end up constructing either a v4 or
|
||||
/// v5 transaction. See [`pezkuwi_subxt_core::tx`] if you'd like to manually pick the version to construct
|
||||
///
|
||||
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_.
|
||||
/// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values.
|
||||
pub fn create_partial_offline<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
let metadata = self.client.metadata();
|
||||
let tx = match pezkuwi_subxt_core::tx::suggested_version(&metadata)? {
|
||||
TransactionVersion::V4 => PartialTransactionInner::V4(
|
||||
pezkuwi_subxt_core::tx::create_v4_signed(call, &self.client.client_state(), params)?,
|
||||
),
|
||||
TransactionVersion::V5 => PartialTransactionInner::V5(
|
||||
pezkuwi_subxt_core::tx::create_v5_general(call, &self.client.client_state(), params)?,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(PartialTransaction {
|
||||
client: self.client.clone(),
|
||||
inner: tx,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a v4 partial transaction, ready to sign.
|
||||
///
|
||||
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_.
|
||||
/// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values.
|
||||
///
|
||||
/// Prefer [`Self::create_partial_offline()`] if you don't know which version to create; this will pick the
|
||||
/// most suitable one for the given chain.
|
||||
pub fn create_v4_partial_offline<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
let tx = PartialTransactionInner::V4(pezkuwi_subxt_core::tx::create_v4_signed(
|
||||
call,
|
||||
&self.client.client_state(),
|
||||
params,
|
||||
)?);
|
||||
|
||||
Ok(PartialTransaction {
|
||||
client: self.client.clone(),
|
||||
inner: tx,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a v5 partial transaction, ready to sign.
|
||||
///
|
||||
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_.
|
||||
/// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values.
|
||||
///
|
||||
/// Prefer [`Self::create_partial_offline()`] if you don't know which version to create; this will pick the
|
||||
/// most suitable one for the given chain.
|
||||
pub fn create_v5_partial_offline<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
let tx = PartialTransactionInner::V5(pezkuwi_subxt_core::tx::create_v5_general(
|
||||
call,
|
||||
&self.client.client_state(),
|
||||
params,
|
||||
)?);
|
||||
|
||||
Ok(PartialTransaction {
|
||||
client: self.client.clone(),
|
||||
inner: tx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> TxClient<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientT<T>,
|
||||
{
|
||||
/// Get the account nonce for a given account ID.
|
||||
pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result<u64, ExtrinsicError> {
|
||||
let block_ref = self
|
||||
.client
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
|
||||
|
||||
crate::blocks::get_account_nonce(&self.client, account_id, block_ref.hash())
|
||||
.await
|
||||
.map_err(|e| ExtrinsicError::AccountNonceError {
|
||||
block_hash: block_ref.hash().into(),
|
||||
account_id: account_id.encode().into(),
|
||||
reason: e,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a partial transaction, without submitting it. This can then be signed and submitted.
|
||||
pub async fn create_partial<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
account_id: &T::AccountId,
|
||||
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
inject_account_nonce_and_block(&self.client, account_id, &mut params).await?;
|
||||
self.create_partial_offline(call, params)
|
||||
}
|
||||
|
||||
/// Creates a partial V4 transaction, without submitting it. This can then be signed and submitted.
|
||||
///
|
||||
/// Prefer [`Self::create_partial()`] if you don't know which version to create; this will pick the
|
||||
/// most suitable one for the given chain.
|
||||
pub async fn create_v4_partial<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
account_id: &T::AccountId,
|
||||
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
inject_account_nonce_and_block(&self.client, account_id, &mut params).await?;
|
||||
self.create_v4_partial_offline(call, params)
|
||||
}
|
||||
|
||||
/// Creates a partial V5 transaction, without submitting it. This can then be signed and submitted.
|
||||
///
|
||||
/// Prefer [`Self::create_partial()`] if you don't know which version to create; this will pick the
|
||||
/// most suitable one for the given chain.
|
||||
pub async fn create_v5_partial<Call>(
|
||||
&self,
|
||||
call: &Call,
|
||||
account_id: &T::AccountId,
|
||||
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
{
|
||||
inject_account_nonce_and_block(&self.client, account_id, &mut params).await?;
|
||||
self.create_v5_partial_offline(call, params)
|
||||
}
|
||||
|
||||
/// Creates a signed transaction, without submitting it.
|
||||
pub async fn create_signed<Call, Signer>(
|
||||
&mut self,
|
||||
call: &Call,
|
||||
signer: &Signer,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
Signer: SignerT<T>,
|
||||
{
|
||||
let mut partial = self
|
||||
.create_partial(call, &signer.account_id(), params)
|
||||
.await?;
|
||||
|
||||
Ok(partial.sign(signer))
|
||||
}
|
||||
|
||||
/// Creates and signs an transaction and submits it to the chain. Passes default parameters
|
||||
/// to construct the "signed extra" and "additional" payloads needed by the transaction.
|
||||
///
|
||||
/// Returns a [`TxProgress`], which can be used to track the status of the transaction
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn sign_and_submit_then_watch_default<Call, Signer>(
|
||||
&mut self,
|
||||
call: &Call,
|
||||
signer: &Signer,
|
||||
) -> Result<TxProgress<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
Signer: SignerT<T>,
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T>>::Params: DefaultParams,
|
||||
{
|
||||
self.sign_and_submit_then_watch(call, signer, DefaultParams::default_params())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an transaction and submits it to the chain.
|
||||
///
|
||||
/// Returns a [`TxProgress`], which can be used to track the status of the transaction
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn sign_and_submit_then_watch<Call, Signer>(
|
||||
&mut self,
|
||||
call: &Call,
|
||||
signer: &Signer,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<TxProgress<T, C>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
Signer: SignerT<T>,
|
||||
{
|
||||
self.create_signed(call, signer, params)
|
||||
.await?
|
||||
.submit_and_watch()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an transaction and submits to the chain for block inclusion. Passes
|
||||
/// default parameters to construct the "signed extra" and "additional" payloads needed
|
||||
/// by the transaction.
|
||||
///
|
||||
/// Returns `Ok` with the transaction hash if it is valid transaction.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Success does not mean the transaction has been included in the block, just that it is valid
|
||||
/// and has been included in the transaction pool.
|
||||
pub async fn sign_and_submit_default<Call, Signer>(
|
||||
&mut self,
|
||||
call: &Call,
|
||||
signer: &Signer,
|
||||
) -> Result<HashFor<T>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
Signer: SignerT<T>,
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T>>::Params: DefaultParams,
|
||||
{
|
||||
self.sign_and_submit(call, signer, DefaultParams::default_params())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates and signs an transaction and submits to the chain for block inclusion.
|
||||
///
|
||||
/// Returns `Ok` with the transaction hash if it is valid transaction.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Success does not mean the transaction has been included in the block, just that it is valid
|
||||
/// and has been included in the transaction pool.
|
||||
pub async fn sign_and_submit<Call, Signer>(
|
||||
&mut self,
|
||||
call: &Call,
|
||||
signer: &Signer,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<HashFor<T>, ExtrinsicError>
|
||||
where
|
||||
Call: Payload,
|
||||
Signer: SignerT<T>,
|
||||
{
|
||||
self.create_signed(call, signer, params)
|
||||
.await?
|
||||
.submit()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// This payload contains the information needed to produce an transaction.
|
||||
pub struct PartialTransaction<T: Config, C> {
|
||||
client: C,
|
||||
inner: PartialTransactionInner<T>,
|
||||
}
|
||||
|
||||
enum PartialTransactionInner<T: Config> {
|
||||
V4(pezkuwi_subxt_core::tx::PartialTransactionV4<T>),
|
||||
V5(pezkuwi_subxt_core::tx::PartialTransactionV5<T>),
|
||||
}
|
||||
|
||||
impl<T, C> PartialTransaction<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OfflineClientT<T>,
|
||||
{
|
||||
/// Return the signer payload for this transaction. These are the bytes that must
|
||||
/// be signed in order to produce a valid signature for the transaction.
|
||||
pub fn signer_payload(&self) -> Vec<u8> {
|
||||
match &self.inner {
|
||||
PartialTransactionInner::V4(tx) => tx.signer_payload(),
|
||||
PartialTransactionInner::V5(tx) => tx.signer_payload().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the bytes representing the call data for this partially constructed
|
||||
/// transaction.
|
||||
pub fn call_data(&self) -> &[u8] {
|
||||
match &self.inner {
|
||||
PartialTransactionInner::V4(tx) => tx.call_data(),
|
||||
PartialTransactionInner::V5(tx) => tx.call_data(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransaction`] into a [`SubmittableTransaction`], ready to submit.
|
||||
/// The provided `signer` is responsible for providing the "from" address for the transaction,
|
||||
/// as well as providing a signature to attach to it.
|
||||
pub fn sign<Signer>(&mut self, signer: &Signer) -> SubmittableTransaction<T, C>
|
||||
where
|
||||
Signer: SignerT<T>,
|
||||
{
|
||||
let tx = match &mut self.inner {
|
||||
PartialTransactionInner::V4(tx) => tx.sign(signer),
|
||||
PartialTransactionInner::V5(tx) => tx.sign(signer),
|
||||
};
|
||||
|
||||
SubmittableTransaction {
|
||||
client: self.client.clone(),
|
||||
inner: tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransaction`] into a [`SubmittableTransaction`], ready to submit.
|
||||
/// An address, and something representing a signature that can be SCALE encoded, are both
|
||||
/// needed in order to construct it. If you have a `Signer` to hand, you can use
|
||||
/// [`PartialTransaction::sign()`] instead.
|
||||
pub fn sign_with_account_and_signature(
|
||||
&mut self,
|
||||
account_id: &T::AccountId,
|
||||
signature: &T::Signature,
|
||||
) -> SubmittableTransaction<T, C> {
|
||||
let tx = match &mut self.inner {
|
||||
PartialTransactionInner::V4(tx) => {
|
||||
tx.sign_with_account_and_signature(account_id.clone(), signature)
|
||||
}
|
||||
PartialTransactionInner::V5(tx) => {
|
||||
tx.sign_with_account_and_signature(account_id, signature)
|
||||
}
|
||||
};
|
||||
|
||||
SubmittableTransaction {
|
||||
client: self.client.clone(),
|
||||
inner: tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents an transaction that has been signed and is ready to submit.
|
||||
pub struct SubmittableTransaction<T, C> {
|
||||
client: C,
|
||||
inner: pezkuwi_subxt_core::tx::Transaction<T>,
|
||||
}
|
||||
|
||||
impl<T, C> SubmittableTransaction<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OfflineClientT<T>,
|
||||
{
|
||||
/// Create a [`SubmittableTransaction`] from some already-signed and prepared
|
||||
/// transaction bytes, and some client (anything implementing [`OfflineClientT`]
|
||||
/// or [`OnlineClientT`]).
|
||||
///
|
||||
/// Prefer to use [`TxClient`] to create and sign transactions. This is simply
|
||||
/// exposed in case you want to skip this process and submit something you've
|
||||
/// already created.
|
||||
pub fn from_bytes(client: C, tx_bytes: Vec<u8>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
inner: pezkuwi_subxt_core::tx::Transaction::from_bytes(tx_bytes),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate and return the hash of the transaction, based on the configured hasher.
|
||||
pub fn hash(&self) -> HashFor<T> {
|
||||
self.inner.hash_with(self.client.hasher())
|
||||
}
|
||||
|
||||
/// Returns the SCALE encoded transaction bytes.
|
||||
pub fn encoded(&self) -> &[u8] {
|
||||
self.inner.encoded()
|
||||
}
|
||||
|
||||
/// Consumes [`SubmittableTransaction`] and returns the SCALE encoded
|
||||
/// transaction bytes.
|
||||
pub fn into_encoded(self) -> Vec<u8> {
|
||||
self.inner.into_encoded()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> SubmittableTransaction<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientT<T>,
|
||||
{
|
||||
/// Submits the transaction to the chain.
|
||||
///
|
||||
/// Returns a [`TxProgress`], which can be used to track the status of the transaction
|
||||
/// and obtain details about it, once it has made it into a block.
|
||||
pub async fn submit_and_watch(&self) -> Result<TxProgress<T, C>, ExtrinsicError> {
|
||||
// Get a hash of the transaction (we'll need this later).
|
||||
let ext_hash = self.hash();
|
||||
|
||||
// Submit and watch for transaction progress.
|
||||
let sub = self
|
||||
.client
|
||||
.backend()
|
||||
.submit_transaction(self.encoded())
|
||||
.await
|
||||
.map_err(ExtrinsicError::ErrorSubmittingTransaction)?;
|
||||
|
||||
Ok(TxProgress::new(sub, self.client.clone(), ext_hash))
|
||||
}
|
||||
|
||||
/// Submits the transaction to the chain for block inclusion.
|
||||
///
|
||||
/// It's usually better to call `submit_and_watch` to get an idea of the progress of the
|
||||
/// submission and whether it's eventually successful or not. This call does not guarantee
|
||||
/// success, and is just sending the transaction to the chain.
|
||||
pub async fn submit(&self) -> Result<HashFor<T>, ExtrinsicError> {
|
||||
let ext_hash = self.hash();
|
||||
let mut sub = self
|
||||
.client
|
||||
.backend()
|
||||
.submit_transaction(self.encoded())
|
||||
.await
|
||||
.map_err(ExtrinsicError::ErrorSubmittingTransaction)?;
|
||||
|
||||
// If we get a bad status or error back straight away then error, else return the hash.
|
||||
match sub.next().await {
|
||||
Some(Ok(status)) => match status {
|
||||
TransactionStatus::Validated
|
||||
| TransactionStatus::Broadcasted
|
||||
| TransactionStatus::InBestBlock { .. }
|
||||
| TransactionStatus::NoLongerInBestBlock
|
||||
| TransactionStatus::InFinalizedBlock { .. } => Ok(ext_hash),
|
||||
TransactionStatus::Error { message } => Err(
|
||||
ExtrinsicError::TransactionStatusError(TransactionStatusError::Error(message)),
|
||||
),
|
||||
TransactionStatus::Invalid { message } => {
|
||||
Err(ExtrinsicError::TransactionStatusError(
|
||||
TransactionStatusError::Invalid(message),
|
||||
))
|
||||
}
|
||||
TransactionStatus::Dropped { message } => {
|
||||
Err(ExtrinsicError::TransactionStatusError(
|
||||
TransactionStatusError::Dropped(message),
|
||||
))
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => Err(ExtrinsicError::TransactionStatusStreamError(e)),
|
||||
None => Err(ExtrinsicError::UnexpectedEndOfTransactionStatusStream),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate a transaction by submitting it to the relevant Runtime API. A transaction that is
|
||||
/// valid can be added to a block, but may still end up in an error state.
|
||||
///
|
||||
/// Returns `Ok` with a [`ValidationResult`], which is the result of attempting to dry run the transaction.
|
||||
pub async fn validate(&self) -> Result<ValidationResult, ExtrinsicError> {
|
||||
let latest_block_ref = self
|
||||
.client
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
|
||||
self.validate_at(latest_block_ref).await
|
||||
}
|
||||
|
||||
/// Validate a transaction by submitting it to the relevant Runtime API. A transaction that is
|
||||
/// valid can be added to a block, but may still end up in an error state.
|
||||
///
|
||||
/// Returns `Ok` with a [`ValidationResult`], which is the result of attempting to dry run the transaction.
|
||||
pub async fn validate_at(
|
||||
&self,
|
||||
at: impl Into<BlockRef<HashFor<T>>>,
|
||||
) -> Result<ValidationResult, ExtrinsicError> {
|
||||
let block_hash = at.into().hash();
|
||||
|
||||
// Approach taken from https://github.com/paritytech/json-rpc-interface-spec/issues/55.
|
||||
let mut params = Vec::with_capacity(8 + self.encoded().len() + 8);
|
||||
2u8.encode_to(&mut params);
|
||||
params.extend(self.encoded().iter());
|
||||
block_hash.encode_to(&mut params);
|
||||
|
||||
let res: Vec<u8> = self
|
||||
.client
|
||||
.backend()
|
||||
.call(
|
||||
"TaggedTransactionQueue_validate_transaction",
|
||||
Some(¶ms),
|
||||
block_hash,
|
||||
)
|
||||
.await
|
||||
.map_err(ExtrinsicError::CannotGetValidationInfo)?;
|
||||
|
||||
ValidationResult::try_from_bytes(res)
|
||||
}
|
||||
|
||||
/// This returns an estimate for what the transaction is expected to cost to execute, less any tips.
|
||||
/// The actual amount paid can vary from block to block based on node traffic and other factors.
|
||||
pub async fn partial_fee_estimate(&self) -> Result<u128, ExtrinsicError> {
|
||||
let mut params = self.encoded().to_vec();
|
||||
(self.encoded().len() as u32).encode_to(&mut params);
|
||||
let latest_block_ref = self
|
||||
.client
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
|
||||
|
||||
// destructuring RuntimeDispatchInfo, see type information <https://paritytech.github.io/substrate/master/pallet_transaction_payment_rpc_runtime_api/struct.RuntimeDispatchInfo.html>
|
||||
// data layout: {weight_ref_time: Compact<u64>, weight_proof_size: Compact<u64>, class: u8, partial_fee: u128}
|
||||
let (_, _, _, partial_fee) = self
|
||||
.client
|
||||
.backend()
|
||||
.call_decoding::<(Compact<u64>, Compact<u64>, u8, u128)>(
|
||||
"TransactionPaymentApi_query_info",
|
||||
Some(¶ms),
|
||||
latest_block_ref.hash(),
|
||||
)
|
||||
.await
|
||||
.map_err(ExtrinsicError::CannotGetFeeInfo)?;
|
||||
|
||||
Ok(partial_fee)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the latest block header and account nonce from the backend and use them to refine [`ExtrinsicParams::Params`].
|
||||
async fn inject_account_nonce_and_block<T: Config, Client: OnlineClientT<T>>(
|
||||
client: &Client,
|
||||
account_id: &T::AccountId,
|
||||
params: &mut <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<(), ExtrinsicError> {
|
||||
use pezkuwi_subxt_core::config::transaction_extensions::Params;
|
||||
|
||||
let block_ref = client
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
|
||||
|
||||
let (block_header, account_nonce) = try_join(
|
||||
client
|
||||
.backend()
|
||||
.block_header(block_ref.hash())
|
||||
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock),
|
||||
crate::blocks::get_account_nonce(client, account_id, block_ref.hash()).map_err(|e| {
|
||||
ExtrinsicError::AccountNonceError {
|
||||
block_hash: block_ref.hash().into(),
|
||||
account_id: account_id.encode().into(),
|
||||
reason: e,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let block_header = block_header.ok_or_else(|| ExtrinsicError::CannotFindBlockHeader {
|
||||
block_hash: block_ref.hash().into(),
|
||||
})?;
|
||||
|
||||
params.inject_account_nonce(account_nonce);
|
||||
params.inject_block(block_header.number().into(), block_ref.hash());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl ValidationResult {
|
||||
#[allow(clippy::get_first)]
|
||||
fn try_from_bytes(bytes: Vec<u8>) -> Result<ValidationResult, ExtrinsicError> {
|
||||
// TaggedTransactionQueue_validate_transaction returns this:
|
||||
// https://github.com/paritytech/substrate/blob/0cdf7029017b70b7c83c21a4dc0aa1020e7914f6/primitives/runtime/src/transaction_validity.rs#L210
|
||||
// We copy some of the inner types and put the three states (valid, invalid, unknown) into one enum,
|
||||
// because from our perspective, the call was successful regardless.
|
||||
if bytes.get(0) == Some(&0) {
|
||||
// ok: valid. Decode but, for now we discard most of the information
|
||||
let res = TransactionValid::decode(&mut &bytes[1..])
|
||||
.map_err(ExtrinsicError::CannotDecodeValidationResult)?;
|
||||
Ok(ValidationResult::Valid(res))
|
||||
} else if bytes.get(0) == Some(&1) && bytes.get(1) == Some(&0) {
|
||||
// error: invalid
|
||||
let res = TransactionInvalid::decode(&mut &bytes[2..])
|
||||
.map_err(ExtrinsicError::CannotDecodeValidationResult)?;
|
||||
Ok(ValidationResult::Invalid(res))
|
||||
} else if bytes.get(0) == Some(&1) && bytes.get(1) == Some(&1) {
|
||||
// error: unknown
|
||||
let res = TransactionUnknown::decode(&mut &bytes[2..])
|
||||
.map_err(ExtrinsicError::CannotDecodeValidationResult)?;
|
||||
Ok(ValidationResult::Unknown(res))
|
||||
} else {
|
||||
// unable to decode the bytes; they aren't what we expect.
|
||||
Err(ExtrinsicError::UnexpectedValidationResultBytes(bytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of performing [`SubmittableTransaction::validate()`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ValidationResult {
|
||||
/// The transaction is valid
|
||||
Valid(TransactionValid),
|
||||
/// The transaction is invalid
|
||||
Invalid(TransactionInvalid),
|
||||
/// Unable to validate the transaction
|
||||
Unknown(TransactionUnknown),
|
||||
}
|
||||
|
||||
impl ValidationResult {
|
||||
/// Is the transaction valid.
|
||||
pub fn is_valid(&self) -> bool {
|
||||
matches!(self, ValidationResult::Valid(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction is valid; here is some more information about it.
|
||||
#[derive(Decode, Clone, Debug, PartialEq)]
|
||||
pub struct TransactionValid {
|
||||
/// Priority of the transaction.
|
||||
///
|
||||
/// Priority determines the ordering of two transactions that have all
|
||||
/// their dependencies (required tags) satisfied.
|
||||
pub priority: u64,
|
||||
/// Transaction dependencies
|
||||
///
|
||||
/// A non-empty list signifies that some other transactions which provide
|
||||
/// given tags are required to be included before that one.
|
||||
pub requires: Vec<Vec<u8>>,
|
||||
/// Provided tags
|
||||
///
|
||||
/// A list of tags this transaction provides. Successfully importing the transaction
|
||||
/// will enable other transactions that depend on (require) those tags to be included as well.
|
||||
/// Provided and required tags allow Substrate to build a dependency graph of transactions
|
||||
/// and import them in the right (linear) order.
|
||||
pub provides: Vec<Vec<u8>>,
|
||||
/// Transaction longevity
|
||||
///
|
||||
/// Longevity describes minimum number of blocks the validity is correct.
|
||||
/// After this period transaction should be removed from the pool or revalidated.
|
||||
pub longevity: u64,
|
||||
/// A flag indicating if the transaction should be propagated to other peers.
|
||||
///
|
||||
/// By setting `false` here the transaction will still be considered for
|
||||
/// including in blocks that are authored on the current node, but will
|
||||
/// never be sent to other peers.
|
||||
pub propagate: bool,
|
||||
}
|
||||
|
||||
/// The runtime was unable to validate the transaction.
|
||||
#[derive(Decode, Clone, Debug, PartialEq)]
|
||||
pub enum TransactionUnknown {
|
||||
/// Could not lookup some information that is required to validate the transaction.
|
||||
CannotLookup,
|
||||
/// No validator found for the given unsigned transaction.
|
||||
NoUnsignedValidator,
|
||||
/// Any other custom unknown validity that is not covered by this enum.
|
||||
Custom(u8),
|
||||
}
|
||||
|
||||
/// The transaction is invalid.
|
||||
#[derive(Decode, Clone, Debug, PartialEq)]
|
||||
pub enum TransactionInvalid {
|
||||
/// The call of the transaction is not expected.
|
||||
Call,
|
||||
/// General error to do with the inability to pay some fees (e.g. account balance too low).
|
||||
Payment,
|
||||
/// General error to do with the transaction not yet being valid (e.g. nonce too high).
|
||||
Future,
|
||||
/// General error to do with the transaction being outdated (e.g. nonce too low).
|
||||
Stale,
|
||||
/// General error to do with the transaction's proofs (e.g. signature).
|
||||
///
|
||||
/// # Possible causes
|
||||
///
|
||||
/// When using a signed extension that provides additional data for signing, it is required
|
||||
/// that the signing and the verifying side use the same additional data. Additional
|
||||
/// data will only be used to generate the signature, but will not be part of the transaction
|
||||
/// itself. As the verifying side does not know which additional data was used while signing
|
||||
/// it will only be able to assume a bad signature and cannot express a more meaningful error.
|
||||
BadProof,
|
||||
/// The transaction birth block is ancient.
|
||||
///
|
||||
/// # Possible causes
|
||||
///
|
||||
/// 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.)
|
||||
AncientBirthBlock,
|
||||
/// The transaction would exhaust the resources of current block.
|
||||
///
|
||||
/// The transaction might be valid, but there are not enough resources
|
||||
/// left in the current block.
|
||||
ExhaustsResources,
|
||||
/// Any other custom invalid validity that is not covered by this enum.
|
||||
Custom(u8),
|
||||
/// An transaction with a Mandatory dispatch resulted in Error. This is indicative of either a
|
||||
/// malicious validator or a buggy `provide_inherent`. In any case, it can result in
|
||||
/// dangerously overweight blocks and therefore if found, invalidates the block.
|
||||
BadMandatory,
|
||||
/// An transaction with a mandatory dispatch tried to be validated.
|
||||
/// This is invalid; only inherent transactions are allowed to have mandatory dispatches.
|
||||
MandatoryValidation,
|
||||
/// The sending address is disabled or known to be invalid.
|
||||
BadSigner,
|
||||
}
|
||||
|
||||
/// This trait is used to create default values for extrinsic params. We use this instead of
|
||||
/// [`Default`] because we want to be able to support params which are tuples of more than 12
|
||||
/// entries (which is the maximum tuple size Rust currently implements [`Default`] for on tuples),
|
||||
/// given that we aren't far off having more than 12 transaction extensions already.
|
||||
///
|
||||
/// If you have params which are _not_ a tuple and which you'd like to be instantiated automatically
|
||||
/// when calling [`TxClient::sign_and_submit_default()`] or [`TxClient::sign_and_submit_then_watch_default()`],
|
||||
/// then you'll need to implement this trait for them.
|
||||
pub trait DefaultParams: Sized {
|
||||
/// Instantiate a default instance of the parameters.
|
||||
fn default_params() -> Self;
|
||||
}
|
||||
|
||||
impl<const N: usize, P: Default> DefaultParams for [P; N] {
|
||||
fn default_params() -> Self {
|
||||
core::array::from_fn(|_| P::default())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_default_params_for_tuple {
|
||||
($($ident:ident),+) => {
|
||||
impl <$($ident : Default),+> DefaultParams for ($($ident,)+){
|
||||
fn default_params() -> Self {
|
||||
(
|
||||
$($ident::default(),)+
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const _: () = {
|
||||
impl_default_params_for_tuple!(A);
|
||||
impl_default_params_for_tuple!(A, B);
|
||||
impl_default_params_for_tuple!(A, B, C);
|
||||
impl_default_params_for_tuple!(A, B, C, D);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y);
|
||||
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z);
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn transaction_validity_decoding_empty_bytes() {
|
||||
// No panic should occur decoding empty bytes.
|
||||
let decoded = ValidationResult::try_from_bytes(vec![]);
|
||||
assert!(decoded.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_validity_decoding_is_ok() {
|
||||
use sp_runtime::transaction_validity as sp;
|
||||
use sp_runtime::transaction_validity::TransactionValidity as T;
|
||||
|
||||
let pairs = vec![
|
||||
(
|
||||
T::Ok(sp::ValidTransaction {
|
||||
..Default::default()
|
||||
}),
|
||||
ValidationResult::Valid(TransactionValid {
|
||||
// By default, tx is immortal
|
||||
longevity: u64::MAX,
|
||||
// Default is true
|
||||
propagate: true,
|
||||
priority: 0,
|
||||
provides: vec![],
|
||||
requires: vec![],
|
||||
}),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::BadProof,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::BadProof),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Call,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Call),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Payment,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Payment),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Future,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Future),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Stale,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Stale),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::AncientBirthBlock,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::AncientBirthBlock),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::ExhaustsResources,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::ExhaustsResources),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::BadMandatory,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::BadMandatory),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::MandatoryValidation,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::MandatoryValidation),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::BadSigner,
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::BadSigner),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Invalid(
|
||||
sp::InvalidTransaction::Custom(123),
|
||||
)),
|
||||
ValidationResult::Invalid(TransactionInvalid::Custom(123)),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Unknown(
|
||||
sp::UnknownTransaction::CannotLookup,
|
||||
)),
|
||||
ValidationResult::Unknown(TransactionUnknown::CannotLookup),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Unknown(
|
||||
sp::UnknownTransaction::NoUnsignedValidator,
|
||||
)),
|
||||
ValidationResult::Unknown(TransactionUnknown::NoUnsignedValidator),
|
||||
),
|
||||
(
|
||||
T::Err(sp::TransactionValidityError::Unknown(
|
||||
sp::UnknownTransaction::Custom(123),
|
||||
)),
|
||||
ValidationResult::Unknown(TransactionUnknown::Custom(123)),
|
||||
),
|
||||
];
|
||||
|
||||
for (sp, validation_result) in pairs {
|
||||
let encoded = sp.encode();
|
||||
let decoded = ValidationResult::try_from_bytes(encoded).expect("should decode OK");
|
||||
assert_eq!(decoded, validation_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Types representing extrinsics/transactions that have been submitted to a node.
|
||||
|
||||
use std::task::Poll;
|
||||
|
||||
use crate::{
|
||||
backend::{BlockRef, StreamOfResults, TransactionStatus as BackendTxStatus},
|
||||
client::OnlineClientT,
|
||||
config::{Config, HashFor},
|
||||
error::{
|
||||
DispatchError, TransactionEventsError, TransactionFinalizedSuccessError,
|
||||
TransactionProgressError, TransactionStatusError,
|
||||
},
|
||||
events::EventsClient,
|
||||
utils::strip_compact_prefix,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use futures::{Stream, StreamExt};
|
||||
|
||||
/// This struct represents a subscription to the progress of some transaction.
|
||||
pub struct TxProgress<T: Config, C> {
|
||||
sub: Option<StreamOfResults<BackendTxStatus<HashFor<T>>>>,
|
||||
ext_hash: HashFor<T>,
|
||||
client: C,
|
||||
}
|
||||
|
||||
impl<T: Config, C> std::fmt::Debug for TxProgress<T, C> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("TxProgress")
|
||||
.field("sub", &"<subscription>")
|
||||
.field("ext_hash", &self.ext_hash)
|
||||
.field("client", &"<client>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// The above type is not `Unpin` by default unless the generic param `T` is,
|
||||
// so we manually make it clear that Unpin is actually fine regardless of `T`
|
||||
// (we don't care if this moves around in memory while it's "pinned").
|
||||
impl<T: Config, C> Unpin for TxProgress<T, C> {}
|
||||
|
||||
impl<T: Config, C> TxProgress<T, C> {
|
||||
/// Instantiate a new [`TxProgress`] from a custom subscription.
|
||||
pub fn new(
|
||||
sub: StreamOfResults<BackendTxStatus<HashFor<T>>>,
|
||||
client: C,
|
||||
ext_hash: HashFor<T>,
|
||||
) -> Self {
|
||||
Self {
|
||||
sub: Some(sub),
|
||||
client,
|
||||
ext_hash,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the hash of the extrinsic.
|
||||
pub fn extrinsic_hash(&self) -> HashFor<T> {
|
||||
self.ext_hash
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> TxProgress<T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientT<T>,
|
||||
{
|
||||
/// Return the next transaction status when it's emitted. This just delegates to the
|
||||
/// [`futures::Stream`] implementation for [`TxProgress`], but allows you to
|
||||
/// avoid importing that trait if you don't otherwise need it.
|
||||
pub async fn next(&mut self) -> Option<Result<TxStatus<T, C>, TransactionProgressError>> {
|
||||
StreamExt::next(self).await
|
||||
}
|
||||
|
||||
/// Wait for the transaction to be finalized, and return a [`TxInBlock`]
|
||||
/// instance when it is, or an error if there was a problem waiting for finalization.
|
||||
///
|
||||
/// **Note:** consumes `self`. If you'd like to perform multiple actions as the state of the
|
||||
/// transaction progresses, use [`TxProgress::next()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid`/`Usurped`/`Dropped` indicate with some
|
||||
/// probability that the transaction will not make it into a block but there is no guarantee
|
||||
/// that this is true. In those cases the stream is closed however, so you currently have no way to find
|
||||
/// out if they finally made it into a block or not.
|
||||
pub async fn wait_for_finalized(mut self) -> Result<TxInBlock<T, C>, TransactionProgressError> {
|
||||
while let Some(status) = self.next().await {
|
||||
match status? {
|
||||
// Finalized! Return.
|
||||
TxStatus::InFinalizedBlock(s) => return Ok(s),
|
||||
// Error scenarios; return the error.
|
||||
TxStatus::Error { message } => {
|
||||
return Err(TransactionStatusError::Error(message).into());
|
||||
}
|
||||
TxStatus::Invalid { message } => {
|
||||
return Err(TransactionStatusError::Invalid(message).into());
|
||||
}
|
||||
TxStatus::Dropped { message } => {
|
||||
return Err(TransactionStatusError::Dropped(message).into());
|
||||
}
|
||||
// Ignore and wait for next status event:
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
Err(TransactionProgressError::UnexpectedEndOfTransactionStatusStream)
|
||||
}
|
||||
|
||||
/// Wait for the transaction to be finalized, and for the transaction events to indicate
|
||||
/// that the transaction was successful. Returns the events associated with the transaction,
|
||||
/// as well as a couple of other details (block hash and extrinsic hash).
|
||||
///
|
||||
/// **Note:** consumes self. If you'd like to perform multiple actions as progress is made,
|
||||
/// use [`TxProgress::next()`] instead.
|
||||
///
|
||||
/// **Note:** transaction statuses like `Invalid`/`Usurped`/`Dropped` indicate with some
|
||||
/// probability that the transaction will not make it into a block but there is no guarantee
|
||||
/// that this is true. In those cases the stream is closed however, so you currently have no way to find
|
||||
/// out if they finally made it into a block or not.
|
||||
pub async fn wait_for_finalized_success(
|
||||
self,
|
||||
) -> Result<crate::blocks::ExtrinsicEvents<T>, TransactionFinalizedSuccessError> {
|
||||
let evs = self.wait_for_finalized().await?.wait_for_success().await?;
|
||||
Ok(evs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, C: Clone> Stream for TxProgress<T, C> {
|
||||
type Item = Result<TxStatus<T, C>, TransactionProgressError>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
let sub = match self.sub.as_mut() {
|
||||
Some(sub) => sub,
|
||||
None => return Poll::Ready(None),
|
||||
};
|
||||
|
||||
sub.poll_next_unpin(cx)
|
||||
.map_err(TransactionProgressError::CannotGetNextProgressUpdate)
|
||||
.map_ok(|status| {
|
||||
match status {
|
||||
BackendTxStatus::Validated => TxStatus::Validated,
|
||||
BackendTxStatus::Broadcasted => TxStatus::Broadcasted,
|
||||
BackendTxStatus::NoLongerInBestBlock => TxStatus::NoLongerInBestBlock,
|
||||
BackendTxStatus::InBestBlock { hash } => TxStatus::InBestBlock(TxInBlock::new(
|
||||
hash,
|
||||
self.ext_hash,
|
||||
self.client.clone(),
|
||||
)),
|
||||
// These stream events mean that nothing further will be sent:
|
||||
BackendTxStatus::InFinalizedBlock { hash } => {
|
||||
self.sub = None;
|
||||
TxStatus::InFinalizedBlock(TxInBlock::new(
|
||||
hash,
|
||||
self.ext_hash,
|
||||
self.client.clone(),
|
||||
))
|
||||
}
|
||||
BackendTxStatus::Error { message } => {
|
||||
self.sub = None;
|
||||
TxStatus::Error { message }
|
||||
}
|
||||
BackendTxStatus::Invalid { message } => {
|
||||
self.sub = None;
|
||||
TxStatus::Invalid { message }
|
||||
}
|
||||
BackendTxStatus::Dropped { message } => {
|
||||
self.sub = None;
|
||||
TxStatus::Dropped { message }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible transaction statuses returned from our [`TxProgress::next()`] call.
|
||||
#[derive_where(Debug; C)]
|
||||
pub enum TxStatus<T: Config, C> {
|
||||
/// Transaction is part of the future queue.
|
||||
Validated,
|
||||
/// The transaction has been broadcast to other nodes.
|
||||
Broadcasted,
|
||||
/// Transaction is no longer in a best block.
|
||||
NoLongerInBestBlock,
|
||||
/// Transaction has been included in block with given hash.
|
||||
InBestBlock(TxInBlock<T, C>),
|
||||
/// Transaction has been finalized by a finality-gadget, e.g GRANDPA
|
||||
InFinalizedBlock(TxInBlock<T, C>),
|
||||
/// Something went wrong in the node.
|
||||
Error {
|
||||
/// Human readable message; what went wrong.
|
||||
message: String,
|
||||
},
|
||||
/// Transaction is invalid (bad nonce, signature etc).
|
||||
Invalid {
|
||||
/// Human readable message; why was it invalid.
|
||||
message: String,
|
||||
},
|
||||
/// The transaction was dropped.
|
||||
Dropped {
|
||||
/// Human readable message; why was it dropped.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: Config, C> TxStatus<T, C> {
|
||||
/// A convenience method to return the finalized details. Returns
|
||||
/// [`None`] if the enum variant is not [`TxStatus::InFinalizedBlock`].
|
||||
pub fn as_finalized(&self) -> Option<&TxInBlock<T, C>> {
|
||||
match self {
|
||||
Self::InFinalizedBlock(val) => Some(val),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A convenience method to return the best block details. Returns
|
||||
/// [`None`] if the enum variant is not [`TxStatus::InBestBlock`].
|
||||
pub fn as_in_block(&self) -> Option<&TxInBlock<T, C>> {
|
||||
match self {
|
||||
Self::InBestBlock(val) => Some(val),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct represents a transaction that has made it into a block.
|
||||
#[derive_where(Debug; C)]
|
||||
pub struct TxInBlock<T: Config, C> {
|
||||
block_ref: BlockRef<HashFor<T>>,
|
||||
ext_hash: HashFor<T>,
|
||||
client: C,
|
||||
}
|
||||
|
||||
impl<T: Config, C> TxInBlock<T, C> {
|
||||
pub(crate) fn new(block_ref: BlockRef<HashFor<T>>, ext_hash: HashFor<T>, client: C) -> Self {
|
||||
Self {
|
||||
block_ref,
|
||||
ext_hash,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the hash of the block that the transaction has made it into.
|
||||
pub fn block_hash(&self) -> HashFor<T> {
|
||||
self.block_ref.hash()
|
||||
}
|
||||
|
||||
/// Return the hash of the extrinsic that was submitted.
|
||||
pub fn extrinsic_hash(&self) -> HashFor<T> {
|
||||
self.ext_hash
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
|
||||
/// Fetch the events associated with this transaction. If the transaction
|
||||
/// was successful (ie no `ExtrinsicFailed`) events were found, then we return
|
||||
/// the events associated with it. If the transaction was not successful, or
|
||||
/// something else went wrong, we return an error.
|
||||
///
|
||||
/// **Note:** If multiple `ExtrinsicFailed` errors are returned (for instance
|
||||
/// because a pallet chooses to emit one as an event, which is considered
|
||||
/// abnormal behaviour), it is not specified which of the errors is returned here.
|
||||
/// You can use [`TxInBlock::fetch_events`] instead if you'd like to
|
||||
/// work with multiple "error" events.
|
||||
///
|
||||
/// **Note:** This has to download block details from the node and decode events
|
||||
/// from them.
|
||||
pub async fn wait_for_success(
|
||||
&self,
|
||||
) -> Result<crate::blocks::ExtrinsicEvents<T>, TransactionEventsError> {
|
||||
let events = self.fetch_events().await?;
|
||||
|
||||
// Try to find any errors; return the first one we encounter.
|
||||
for (ev_idx, ev) in events.iter().enumerate() {
|
||||
let ev = ev.map_err(|e| TransactionEventsError::CannotDecodeEventInBlock {
|
||||
event_index: ev_idx,
|
||||
block_hash: self.block_hash().into(),
|
||||
error: e,
|
||||
})?;
|
||||
|
||||
if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicFailed" {
|
||||
let dispatch_error =
|
||||
DispatchError::decode_from(ev.field_bytes(), self.client.metadata()).map_err(
|
||||
|e| TransactionEventsError::CannotDecodeDispatchError {
|
||||
error: e,
|
||||
bytes: ev.field_bytes().to_vec(),
|
||||
},
|
||||
)?;
|
||||
return Err(dispatch_error.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// Fetch all of the events associated with this transaction. This succeeds whether
|
||||
/// the transaction was a success or not; it's up to you to handle the error and
|
||||
/// success events however you prefer.
|
||||
///
|
||||
/// **Note:** This has to download block details from the node and decode events
|
||||
/// from them.
|
||||
pub async fn fetch_events(
|
||||
&self,
|
||||
) -> Result<crate::blocks::ExtrinsicEvents<T>, TransactionEventsError> {
|
||||
let hasher = self.client.hasher();
|
||||
|
||||
let block_body = self
|
||||
.client
|
||||
.backend()
|
||||
.block_body(self.block_ref.hash())
|
||||
.await
|
||||
.map_err(|e| TransactionEventsError::CannotFetchBlockBody {
|
||||
block_hash: self.block_hash().into(),
|
||||
error: e,
|
||||
})?
|
||||
.ok_or_else(|| TransactionEventsError::BlockNotFound {
|
||||
block_hash: self.block_hash().into(),
|
||||
})?;
|
||||
|
||||
let extrinsic_idx = block_body
|
||||
.iter()
|
||||
.position(|ext| {
|
||||
use crate::config::Hasher;
|
||||
let Ok((_, stripped)) = strip_compact_prefix(ext) else {
|
||||
return false;
|
||||
};
|
||||
let hash = hasher.hash_of(&stripped);
|
||||
hash == self.ext_hash
|
||||
})
|
||||
// If we successfully obtain the block hash we think contains our
|
||||
// extrinsic, the extrinsic should be in there somewhere..
|
||||
.ok_or_else(|| TransactionEventsError::CannotFindTransactionInBlock {
|
||||
block_hash: self.block_hash().into(),
|
||||
transaction_hash: self.ext_hash.into(),
|
||||
})?;
|
||||
|
||||
let events = EventsClient::new(self.client.clone())
|
||||
.at(self.block_ref.clone())
|
||||
.await
|
||||
.map_err(
|
||||
|e| TransactionEventsError::CannotFetchEventsForTransaction {
|
||||
block_hash: self.block_hash().into(),
|
||||
transaction_hash: self.ext_hash.into(),
|
||||
error: e,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(crate::blocks::ExtrinsicEvents::new(
|
||||
self.ext_hash,
|
||||
extrinsic_idx as u32,
|
||||
events,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pezkuwi_subxt_core::client::RuntimeVersion;
|
||||
|
||||
use crate::{
|
||||
SubstrateConfig,
|
||||
backend::{StreamOfResults, TransactionStatus},
|
||||
client::{OfflineClientT, OnlineClientT},
|
||||
config::{Config, HashFor},
|
||||
tx::TxProgress,
|
||||
};
|
||||
|
||||
type MockTxProgress = TxProgress<SubstrateConfig, MockClient>;
|
||||
type MockHash = HashFor<SubstrateConfig>;
|
||||
type MockSubstrateTxStatus = TransactionStatus<MockHash>;
|
||||
|
||||
/// a mock client to satisfy trait bounds in tests
|
||||
#[derive(Clone, Debug)]
|
||||
struct MockClient;
|
||||
|
||||
impl OfflineClientT<SubstrateConfig> for MockClient {
|
||||
fn metadata(&self) -> crate::Metadata {
|
||||
unimplemented!("just a mock impl to satisfy trait bounds")
|
||||
}
|
||||
|
||||
fn genesis_hash(&self) -> MockHash {
|
||||
unimplemented!("just a mock impl to satisfy trait bounds")
|
||||
}
|
||||
|
||||
fn runtime_version(&self) -> RuntimeVersion {
|
||||
unimplemented!("just a mock impl to satisfy trait bounds")
|
||||
}
|
||||
|
||||
fn hasher(&self) -> <SubstrateConfig as Config>::Hasher {
|
||||
unimplemented!("just a mock impl to satisfy trait bounds")
|
||||
}
|
||||
|
||||
fn client_state(&self) -> pezkuwi_subxt_core::client::ClientState<SubstrateConfig> {
|
||||
unimplemented!("just a mock impl to satisfy trait bounds")
|
||||
}
|
||||
}
|
||||
|
||||
impl OnlineClientT<SubstrateConfig> for MockClient {
|
||||
fn backend(&self) -> &dyn crate::backend::Backend<SubstrateConfig> {
|
||||
unimplemented!("just a mock impl to satisfy trait bounds")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_for_finalized_returns_err_when_error() {
|
||||
let tx_progress = mock_tx_progress(vec![
|
||||
MockSubstrateTxStatus::Broadcasted,
|
||||
MockSubstrateTxStatus::Error {
|
||||
message: "err".into(),
|
||||
},
|
||||
]);
|
||||
let finalized_result = tx_progress.wait_for_finalized().await;
|
||||
assert!(matches!(
|
||||
finalized_result,
|
||||
Err(TransactionProgressError::TransactionStatusError(TransactionStatusError::Error(e))) if e == "err"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_for_finalized_returns_err_when_invalid() {
|
||||
let tx_progress = mock_tx_progress(vec![
|
||||
MockSubstrateTxStatus::Broadcasted,
|
||||
MockSubstrateTxStatus::Invalid {
|
||||
message: "err".into(),
|
||||
},
|
||||
]);
|
||||
let finalized_result = tx_progress.wait_for_finalized().await;
|
||||
assert!(matches!(
|
||||
finalized_result,
|
||||
Err(TransactionProgressError::TransactionStatusError(TransactionStatusError::Invalid(e))) if e == "err"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_for_finalized_returns_err_when_dropped() {
|
||||
let tx_progress = mock_tx_progress(vec![
|
||||
MockSubstrateTxStatus::Broadcasted,
|
||||
MockSubstrateTxStatus::Dropped {
|
||||
message: "err".into(),
|
||||
},
|
||||
]);
|
||||
let finalized_result = tx_progress.wait_for_finalized().await;
|
||||
assert!(matches!(
|
||||
finalized_result,
|
||||
Err(TransactionProgressError::TransactionStatusError(TransactionStatusError::Dropped(e))) if e == "err"
|
||||
));
|
||||
}
|
||||
|
||||
fn mock_tx_progress(statuses: Vec<MockSubstrateTxStatus>) -> MockTxProgress {
|
||||
let sub = create_substrate_tx_status_subscription(statuses);
|
||||
TxProgress::new(sub, MockClient, Default::default())
|
||||
}
|
||||
|
||||
fn create_substrate_tx_status_subscription(
|
||||
elements: Vec<MockSubstrateTxStatus>,
|
||||
) -> StreamOfResults<MockSubstrateTxStatus> {
|
||||
let results = elements.into_iter().map(Ok);
|
||||
let stream = Box::pin(futures::stream::iter(results));
|
||||
let sub: StreamOfResults<MockSubstrateTxStatus> = StreamOfResults::new(stream);
|
||||
sub
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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::macros::{cfg_jsonrpsee_native, cfg_jsonrpsee_web};
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
/// Possible errors encountered trying to fetch a chain spec from an RPC node.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum FetchChainspecError {
|
||||
#[error("Cannot fetch chain spec: RPC error: {0}.")]
|
||||
RpcError(String),
|
||||
#[error("Cannot fetch chain spec: Invalid URL.")]
|
||||
InvalidUrl,
|
||||
#[error("Cannot fetch chain spec: Invalid URL scheme.")]
|
||||
InvalidScheme,
|
||||
#[error("Cannot fetch chain spec: Handshake error establishing WS connection.")]
|
||||
HandshakeError,
|
||||
}
|
||||
|
||||
/// Fetch a chain spec from an RPC node at the given URL.
|
||||
pub async fn fetch_chainspec_from_rpc_node(
|
||||
url: impl AsRef<str>,
|
||||
) -> Result<Box<RawValue>, FetchChainspecError> {
|
||||
use jsonrpsee::core::client::{ClientT, SubscriptionClientT};
|
||||
use jsonrpsee::rpc_params;
|
||||
|
||||
let client = jsonrpsee_helpers::client(url.as_ref()).await?;
|
||||
|
||||
let result = client
|
||||
.request("sync_state_genSyncSpec", jsonrpsee::rpc_params![true])
|
||||
.await
|
||||
.map_err(|err| FetchChainspecError::RpcError(err.to_string()))?;
|
||||
|
||||
// Subscribe to the finalized heads of the chain.
|
||||
let mut subscription = SubscriptionClientT::subscribe::<Box<RawValue>, _>(
|
||||
&client,
|
||||
"chain_subscribeFinalizedHeads",
|
||||
rpc_params![],
|
||||
"chain_unsubscribeFinalizedHeads",
|
||||
)
|
||||
.await
|
||||
.map_err(|err| FetchChainspecError::RpcError(err.to_string()))?;
|
||||
|
||||
// We must ensure that the finalized block of the chain is not the block included
|
||||
// in the chainSpec.
|
||||
// This is a temporary workaround for: https://github.com/smol-dot/smoldot/issues/1562.
|
||||
// The first finalized block that is received might by the finalized block could be the one
|
||||
// included in the chainSpec. Decoding the chainSpec for this purpose is too complex.
|
||||
let _ = subscription.next().await;
|
||||
let _ = subscription.next().await;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
cfg_jsonrpsee_native! {
|
||||
mod jsonrpsee_helpers {
|
||||
use super::FetchChainspecError;
|
||||
use tokio_util::compat::Compat;
|
||||
|
||||
pub use jsonrpsee::{
|
||||
client_transport::ws::{self, EitherStream, Url, WsTransportClientBuilder},
|
||||
core::client::Client,
|
||||
};
|
||||
|
||||
pub type Sender = ws::Sender<Compat<EitherStream>>;
|
||||
pub type Receiver = ws::Receiver<Compat<EitherStream>>;
|
||||
|
||||
/// Build WS RPC client from URL
|
||||
pub async fn client(url: &str) -> Result<Client, FetchChainspecError> {
|
||||
let url = Url::parse(url).map_err(|_| FetchChainspecError::InvalidUrl)?;
|
||||
|
||||
if url.scheme() != "ws" && url.scheme() != "wss" {
|
||||
return Err(FetchChainspecError::InvalidScheme);
|
||||
}
|
||||
|
||||
let (sender, receiver) = ws_transport(url).await?;
|
||||
|
||||
Ok(Client::builder()
|
||||
.max_buffer_capacity_per_subscription(4096)
|
||||
.build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
async fn ws_transport(url: Url) -> Result<(Sender, Receiver), FetchChainspecError> {
|
||||
WsTransportClientBuilder::default()
|
||||
.build(url)
|
||||
.await
|
||||
.map_err(|_| FetchChainspecError::HandshakeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg_jsonrpsee_web! {
|
||||
mod jsonrpsee_helpers {
|
||||
use super::FetchChainspecError;
|
||||
pub use jsonrpsee::{
|
||||
client_transport::web,
|
||||
core::client::{Client, ClientBuilder},
|
||||
};
|
||||
|
||||
/// Build web RPC client from URL
|
||||
pub async fn client(url: &str) -> Result<Client, FetchChainspecError> {
|
||||
let (sender, receiver) = web::connect(url)
|
||||
.await
|
||||
.map_err(|_| FetchChainspecError::HandshakeError)?;
|
||||
|
||||
Ok(ClientBuilder::default()
|
||||
.max_buffer_capacity_per_subscription(4096)
|
||||
.build_with_wasm(sender, receiver))
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Miscellaneous utility helpers.
|
||||
|
||||
use crate::macros::cfg_jsonrpsee;
|
||||
|
||||
pub use pezkuwi_subxt_core::utils::{
|
||||
AccountId32, Encoded, Era, H160, H256, H512, KeyedVec, MultiAddress, MultiSignature,
|
||||
PhantomDataSendSync, Static, UncheckedExtrinsic, WrapperKeepOpaque, Yes, bits,
|
||||
strip_compact_prefix, to_hex,
|
||||
};
|
||||
|
||||
pub use pezkuwi_subxt_rpcs::utils::url_is_secure;
|
||||
|
||||
cfg_jsonrpsee! {
|
||||
mod fetch_chain_spec;
|
||||
pub use fetch_chain_spec::{fetch_chainspec_from_rpc_node, FetchChainspecError};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Types associated with executing View Function calls.
|
||||
|
||||
mod view_function_types;
|
||||
mod view_functions_client;
|
||||
|
||||
pub use pezkuwi_subxt_core::view_functions::payload::{DynamicPayload, Payload, StaticPayload, dynamic};
|
||||
pub use view_function_types::ViewFunctionsApi;
|
||||
pub use view_functions_client::ViewFunctionsClient;
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::Payload;
|
||||
use crate::{
|
||||
backend::BlockRef,
|
||||
client::OnlineClientT,
|
||||
config::{Config, HashFor},
|
||||
error::ViewFunctionError,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
|
||||
/// Execute View Function calls.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct ViewFunctionsApi<T: Config, Client> {
|
||||
client: Client,
|
||||
block_ref: BlockRef<HashFor<T>>,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config, Client> ViewFunctionsApi<T, Client> {
|
||||
/// Create a new [`ViewFunctionsApi`]
|
||||
pub(crate) fn new(client: Client, block_ref: BlockRef<HashFor<T>>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
block_ref,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> ViewFunctionsApi<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Run the validation logic against some View Function payload you'd like to use. Returns `Ok(())`
|
||||
/// if the payload is valid (or if it's not possible to check since the payload has no validation hash).
|
||||
/// Return an error if the payload was not valid or something went wrong trying to validate it (ie
|
||||
/// the View Function in question do not exist at all)
|
||||
pub fn validate<Call: Payload>(&self, payload: Call) -> Result<(), ViewFunctionError> {
|
||||
pezkuwi_subxt_core::view_functions::validate(payload, &self.client.metadata()).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Execute a View Function call.
|
||||
pub fn call<Call: Payload>(
|
||||
&self,
|
||||
payload: Call,
|
||||
) -> impl Future<Output = Result<Call::ReturnType, ViewFunctionError>> + use<Call, Client, T>
|
||||
{
|
||||
let client = self.client.clone();
|
||||
let block_hash = self.block_ref.hash();
|
||||
// Ensure that the returned future doesn't have a lifetime tied to api.view_functions(),
|
||||
// which is a temporary thing we'll be throwing away quickly:
|
||||
async move {
|
||||
let metadata = client.metadata();
|
||||
|
||||
// Validate the View Function payload hash against the compile hash from codegen.
|
||||
pezkuwi_subxt_core::view_functions::validate(&payload, &metadata)?;
|
||||
|
||||
// Assemble the data to call the "execute_view_function" runtime API, which
|
||||
// then calls the relevant view function.
|
||||
let call_name = pezkuwi_subxt_core::view_functions::CALL_NAME;
|
||||
let call_args = pezkuwi_subxt_core::view_functions::call_args(&payload, &metadata)?;
|
||||
|
||||
// Make the call.
|
||||
let bytes = client
|
||||
.backend()
|
||||
.call(call_name, Some(call_args.as_slice()), block_hash)
|
||||
.await
|
||||
.map_err(ViewFunctionError::CannotCallApi)?;
|
||||
|
||||
// Decode the response.
|
||||
let value =
|
||||
pezkuwi_subxt_core::view_functions::decode_value(&mut &*bytes, &payload, &metadata)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::view_function_types::ViewFunctionsApi;
|
||||
|
||||
use crate::{
|
||||
backend::BlockRef,
|
||||
client::OnlineClientT,
|
||||
config::{Config, HashFor},
|
||||
error::ViewFunctionError,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
|
||||
/// Make View Function calls at some block.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct ViewFunctionsClient<T, Client> {
|
||||
client: Client,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> ViewFunctionsClient<T, Client> {
|
||||
/// Create a new [`ViewFunctionsClient`]
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> ViewFunctionsClient<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientT<T>,
|
||||
{
|
||||
/// Obtain an interface to call View Functions at some block hash.
|
||||
pub fn at(&self, block_ref: impl Into<BlockRef<HashFor<T>>>) -> ViewFunctionsApi<T, Client> {
|
||||
ViewFunctionsApi::new(self.client.clone(), block_ref.into())
|
||||
}
|
||||
|
||||
/// Obtain an interface to call View Functions at the latest finalized block.
|
||||
pub fn at_latest(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<ViewFunctionsApi<T, Client>, ViewFunctionError>> + Send + 'static
|
||||
{
|
||||
// Clone and pass the client in like this so that we can explicitly
|
||||
// return a Future that's Send + 'static, rather than tied to &self.
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
// get the ref for the latest finalized block and use that.
|
||||
let block_ref = client
|
||||
.backend()
|
||||
.latest_finalized_block_ref()
|
||||
.await
|
||||
.map_err(ViewFunctionError::CannotGetLatestFinalizedBlock)?;
|
||||
|
||||
Ok(ViewFunctionsApi::new(client, block_ref))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user