mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-31 07:31:02 +00:00
Move subxt-new to be the new subxt, and get first example working
This commit is contained in:
+34
-20
@@ -22,6 +22,14 @@ workspace = true
|
||||
# it's recommended to use `--no-default-features` and then select what you need.
|
||||
default = ["jsonrpsee", "native"]
|
||||
|
||||
# Features that we expect to be enabled for documentation.
|
||||
docs = [
|
||||
"default",
|
||||
"unstable-light-client",
|
||||
"runtime",
|
||||
"reconnecting-rpc-client",
|
||||
]
|
||||
|
||||
# Enable this for native (ie non web/wasm builds).
|
||||
# Exactly 1 of "web" and "native" is expected.
|
||||
native = [
|
||||
@@ -75,21 +83,27 @@ runtime-wasm-path = ["subxt-macro/runtime-wasm-path"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
base58 = { workspace = true }
|
||||
blake2 = { workspace = true }
|
||||
codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] }
|
||||
derive-where = { workspace = true }
|
||||
scale-info = { workspace = true, features = ["default"] }
|
||||
scale-info-legacy = { workspace = true }
|
||||
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 }
|
||||
impl-serde = { workspace = true, default-features = false }
|
||||
keccak-hash = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, features = ["default", "raw_value"] }
|
||||
sp-crypto-hashing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
frame-metadata = { workspace = true }
|
||||
frame-decode = { workspace = true, features = ["legacy-types"] }
|
||||
either = { workspace = true }
|
||||
web-time = { workspace = true }
|
||||
|
||||
@@ -101,10 +115,10 @@ jsonrpsee = { workspace = true, optional = true, features = ["jsonrpsee-types"]
|
||||
|
||||
# Other subxt crates we depend on.
|
||||
subxt-macro = { workspace = true }
|
||||
subxt-core = { workspace = true, features = ["std"] }
|
||||
subxt-metadata = { workspace = true, features = ["std"] }
|
||||
subxt-metadata = { workspace = true, features = ["std", "legacy"] }
|
||||
subxt-lightclient = { workspace = true, optional = true, default-features = false }
|
||||
subxt-rpcs = { workspace = true, features = ["subxt"] }
|
||||
subxt-rpcs = { workspace = true }
|
||||
subxt-utils-accountid32 = { workspace = true }
|
||||
|
||||
# For parsing urls to disallow insecure schemes
|
||||
url = { workspace = true }
|
||||
@@ -127,8 +141,8 @@ sp-core = { workspace = true, features = ["std"] }
|
||||
sp-keyring = { workspace = true, features = ["std"] }
|
||||
sp-runtime = { workspace = true, features = ["std"] }
|
||||
assert_matches = { workspace = true }
|
||||
subxt-signer = { path = "../signer", features = ["unstable-eth"] }
|
||||
subxt-rpcs = { workspace = true, features = ["subxt", "mock-rpc-client"] }
|
||||
subxt-signer = { workspace = true, features = ["unstable-eth", "subxt", "sr25519"] }
|
||||
subxt-rpcs = { workspace = true, features = ["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
|
||||
@@ -140,23 +154,23 @@ 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"]
|
||||
# [[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"]
|
||||
features = ["docs"]
|
||||
|
||||
[package.metadata.playground]
|
||||
features = ["default", "unstable-light-client"]
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
#![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()
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::utils::fetch_chainspec_from_rpc_node;
|
||||
use subxt::{
|
||||
PolkadotConfig,
|
||||
client::OnlineClient,
|
||||
lightclient::{ChainConfig, LightClient},
|
||||
};
|
||||
use 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(())
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::backend::{legacy::LegacyRpcMethods, rpc::RpcClient};
|
||||
use subxt::config::DefaultExtrinsicParamsBuilder as Params;
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use subxt_signer::sr25519::dev;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::utils::AccountId32;
|
||||
use subxt::{OnlineClient, config::PolkadotConfig};
|
||||
use 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(())
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, config::PolkadotConfig};
|
||||
use subxt_signer::sr25519::dev;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 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(())
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::config::{
|
||||
Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, PolkadotConfig, SubstrateConfig,
|
||||
};
|
||||
use 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;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use codec::Encode;
|
||||
use subxt::client::ClientState;
|
||||
use subxt::config::{
|
||||
Config, ExtrinsicParams, ExtrinsicParamsEncoder, ExtrinsicParamsError, HashFor,
|
||||
transaction_extensions::Params,
|
||||
};
|
||||
use 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;
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
#![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 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;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
//! 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(())
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
//! 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(())
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use 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(())
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::dynamic::{At, Value};
|
||||
use subxt::utils::AccountId32;
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use 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(())
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#![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(())
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
//! 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(())
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use subxt::{Error, OnlineClient, PolkadotConfig};
|
||||
use subxt_signer::sr25519::dev;
|
||||
|
||||
// Generate an interface that we can use from the node's metadata.
|
||||
@@ -7,27 +7,33 @@ use subxt_signer::sr25519::dev;
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn main() -> Result<(), Error> {
|
||||
// Create a new API client, configured to talk to Polkadot nodes.
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
let config = PolkadotConfig::new();
|
||||
let api = OnlineClient::new(config).await?;
|
||||
|
||||
// Almost all actions are performed at an explicit block. Here we use
|
||||
// the current block at the time of running this.
|
||||
let at_block = api.at_current_block().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 balance_transfer_tx = polkadot::transactions()
|
||||
.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()
|
||||
let events = at_block
|
||||
.transactions()
|
||||
.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 {
|
||||
if let Some(event) = events.find_first::<polkadot::balances::events::Transfer>() {
|
||||
println!("Balance transfer success: {event:?}");
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
//! 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 subxt_core::utils::AccountId20;
|
||||
use 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(())
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use subxt_signer::sr25519::dev;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
|
||||
// Prepare some extrinsics. These are boxed so that they can live alongside each other.
|
||||
let txs = [dynamic_remark(), balance_transfer(), remark()];
|
||||
|
||||
for tx in txs {
|
||||
let from = dev::alice();
|
||||
api.tx()
|
||||
.sign_and_submit_then_watch_default(&tx, &from)
|
||||
.await?
|
||||
.wait_for_finalized_success()
|
||||
.await?;
|
||||
|
||||
println!("Submitted tx");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn balance_transfer() -> Box<dyn subxt::tx::Payload> {
|
||||
let dest = dev::bob().public_key().into();
|
||||
Box::new(polkadot::tx().balances().transfer_allow_death(dest, 10_000))
|
||||
}
|
||||
|
||||
fn remark() -> Box<dyn subxt::tx::Payload> {
|
||||
Box::new(polkadot::tx().system().remark(vec![1, 2, 3, 4, 5]))
|
||||
}
|
||||
|
||||
fn dynamic_remark() -> Box<dyn subxt::tx::Payload> {
|
||||
use subxt::dynamic::{Value, tx};
|
||||
let tx_payload = tx("System", "remark", vec![Value::from_bytes("Hello")]);
|
||||
|
||||
Box::new(tx_payload)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use subxt_signer::sr25519::dev;
|
||||
|
||||
type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), BoxedError> {
|
||||
// Spawned tasks require things held across await points to impl Send,
|
||||
// so we use one to demonstrate that this is possible with `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(())
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::{OnlineClient, PolkadotConfig, tx::TxStatus};
|
||||
use 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(())
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
use subxt::config::polkadot::PolkadotExtrinsicParamsBuilder as Params;
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
use subxt_signer::sr25519::dev;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
|
||||
pub mod polkadot {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 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,349 @@
|
||||
// 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 backend trait for Subxt which allows us to get and set
|
||||
//! the necessary information (probably from a JSON-RPC API, but that's up to the
|
||||
//! implementation).
|
||||
|
||||
mod archive;
|
||||
mod chain_head;
|
||||
mod combined;
|
||||
mod legacy;
|
||||
mod utils;
|
||||
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::error::BackendError;
|
||||
use async_trait::async_trait;
|
||||
use codec::{Decode, Encode};
|
||||
use futures::{Stream, StreamExt};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use subxt_metadata::Metadata;
|
||||
|
||||
// Expose our various backends.
|
||||
pub use archive::ArchiveBackend;
|
||||
pub use chain_head::{ChainHeadBackend, ChainHeadBackendBuilder, ChainHeadBackendDriver};
|
||||
pub use combined::{CombinedBackend, CombinedBackendBuilder, CombinedBackendDriver};
|
||||
pub use legacy::{LegacyBackend, LegacyBackendBuilder};
|
||||
|
||||
/// Prevent the backend trait being implemented externally.
|
||||
#[doc(hidden)]
|
||||
pub(crate) mod sealed {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
/// This trait exposes the interface that Subxt will use to communicate with
|
||||
/// a backend. Its goal is to be as minimal as possible.
|
||||
#[async_trait]
|
||||
pub trait Backend<T: Config>: sealed::Sealed + Send + Sync + 'static {
|
||||
/// Fetch values from storage.
|
||||
async fn storage_fetch_values(
|
||||
&self,
|
||||
keys: Vec<Vec<u8>>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError>;
|
||||
|
||||
/// Fetch keys underneath the given key from storage.
|
||||
async fn storage_fetch_descendant_keys(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<Vec<u8>>, BackendError>;
|
||||
|
||||
/// Fetch values underneath the given key from storage.
|
||||
async fn storage_fetch_descendant_values(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError>;
|
||||
|
||||
/// Fetch the genesis hash
|
||||
async fn genesis_hash(&self) -> Result<HashFor<T>, BackendError>;
|
||||
|
||||
/// Convert a block number to a hash. This should return `None` in the event that
|
||||
/// multiple block hashes correspond to the given number (ie if the number is greater
|
||||
/// than that of the latest finalized block and some forks exist). Nevertheless, it could
|
||||
/// still return the hash to a block on some fork that is pruned.
|
||||
async fn block_number_to_hash(
|
||||
&self,
|
||||
number: u64,
|
||||
) -> Result<Option<BlockRef<HashFor<T>>>, BackendError>;
|
||||
|
||||
/// Get a block header.
|
||||
async fn block_header(&self, at: HashFor<T>) -> Result<Option<T::Header>, BackendError>;
|
||||
|
||||
/// Return the extrinsics found in the block. Each extrinsic is represented
|
||||
/// by a vector of bytes which has _not_ been SCALE decoded (in other words, the
|
||||
/// first bytes in the vector will decode to the compact encoded length of the extrinsic)
|
||||
async fn block_body(&self, at: HashFor<T>) -> Result<Option<Vec<Vec<u8>>>, BackendError>;
|
||||
|
||||
/// Get the most recent finalized block hash.
|
||||
/// Note: needed only in blocks client for finalized block stream; can prolly be removed.
|
||||
async fn latest_finalized_block_ref(&self) -> Result<BlockRef<HashFor<T>>, BackendError>;
|
||||
|
||||
/// A stream of all new block headers as they arrive.
|
||||
async fn stream_all_block_headers(
|
||||
&self,
|
||||
hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError>;
|
||||
|
||||
/// A stream of best block headers.
|
||||
async fn stream_best_block_headers(
|
||||
&self,
|
||||
hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError>;
|
||||
|
||||
/// A stream of finalized block headers.
|
||||
async fn stream_finalized_block_headers(
|
||||
&self,
|
||||
hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError>;
|
||||
|
||||
/// Submit a transaction. This will return a stream of events about it.
|
||||
async fn submit_transaction(
|
||||
&self,
|
||||
bytes: &[u8],
|
||||
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError>;
|
||||
|
||||
/// Make a call to some runtime API.
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<Vec<u8>, BackendError>;
|
||||
}
|
||||
|
||||
/// helpful utility methods derived from those provided on [`Backend`]
|
||||
#[async_trait]
|
||||
pub trait BackendExt<T: Config>: Backend<T> {
|
||||
/// Fetch a single value from storage.
|
||||
async fn storage_fetch_value(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<Option<Vec<u8>>, BackendError> {
|
||||
self.storage_fetch_values(vec![key], at)
|
||||
.await?
|
||||
.next()
|
||||
.await
|
||||
.transpose()
|
||||
.map(|o| o.map(|s| s.value))
|
||||
}
|
||||
|
||||
/// The same as a [`Backend::call()`], but it will also attempt to decode the
|
||||
/// result into the given type, which is a fairly common operation.
|
||||
async fn call_decoding<D: codec::Decode>(
|
||||
&self,
|
||||
method: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<D, BackendError> {
|
||||
let bytes = self.call(method, call_parameters, at).await?;
|
||||
let res =
|
||||
D::decode(&mut &*bytes).map_err(BackendError::CouldNotScaleDecodeRuntimeResponse)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Return the metadata at some version.
|
||||
async fn metadata_at_version(
|
||||
&self,
|
||||
version: u32,
|
||||
at: HashFor<T>,
|
||||
) -> Result<Metadata, BackendError> {
|
||||
let param = version.encode();
|
||||
|
||||
let opaque: Option<frame_metadata::OpaqueMetadata> = self
|
||||
.call_decoding("Metadata_metadata_at_version", Some(¶m), at)
|
||||
.await?;
|
||||
let Some(opaque) = opaque else {
|
||||
return Err(BackendError::MetadataVersionNotFound(version));
|
||||
};
|
||||
|
||||
let metadata: Metadata =
|
||||
Decode::decode(&mut &opaque.0[..]).map_err(BackendError::CouldNotDecodeMetadata)?;
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Return V14 metadata from the legacy `Metadata_metadata` call.
|
||||
async fn legacy_metadata(&self, at: HashFor<T>) -> Result<Metadata, BackendError> {
|
||||
let opaque: frame_metadata::OpaqueMetadata =
|
||||
self.call_decoding("Metadata_metadata", None, at).await?;
|
||||
let metadata: Metadata =
|
||||
Decode::decode(&mut &opaque.0[..]).map_err(BackendError::CouldNotDecodeMetadata)?;
|
||||
Ok(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B: Backend<T> + ?Sized, T: Config> BackendExt<T> for B {}
|
||||
|
||||
/// An opaque struct which, while alive, indicates that some references to a block
|
||||
/// still exist. This gives the backend the opportunity to keep the corresponding block
|
||||
/// details around for a while if it likes and is able to. No guarantees can be made about
|
||||
/// how long the corresponding details might be available for, but if no references to a block
|
||||
/// exist, then the backend is free to discard any details for it.
|
||||
#[derive(Clone)]
|
||||
pub struct BlockRef<H> {
|
||||
hash: H,
|
||||
// We keep this around so that when it is dropped, it has the
|
||||
// opportunity to tell the backend.
|
||||
_pointer: Option<Arc<dyn BlockRefT>>,
|
||||
}
|
||||
|
||||
impl<H> From<H> for BlockRef<H> {
|
||||
fn from(value: H) -> Self {
|
||||
BlockRef::from_hash(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: PartialEq> PartialEq for BlockRef<H> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.hash == other.hash
|
||||
}
|
||||
}
|
||||
impl<H: Eq> Eq for BlockRef<H> {}
|
||||
|
||||
// Manual implementation to work around https://github.com/mcarton/rust-derivative/issues/115.
|
||||
impl<H: PartialOrd> PartialOrd for BlockRef<H> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.hash.partial_cmp(&other.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Ord> Ord for BlockRef<H> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.hash.cmp(&other.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: std::fmt::Debug> std::fmt::Debug for BlockRef<H> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("BlockRef").field(&self.hash).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: std::hash::Hash> std::hash::Hash for BlockRef<H> {
|
||||
fn hash<Hasher: std::hash::Hasher>(&self, state: &mut Hasher) {
|
||||
self.hash.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> BlockRef<H> {
|
||||
/// A [`BlockRef`] that doesn't reference a given block, but does have an associated hash.
|
||||
/// This is used in the legacy backend, which has no notion of pinning blocks.
|
||||
pub fn from_hash(hash: H) -> Self {
|
||||
Self {
|
||||
hash,
|
||||
_pointer: None,
|
||||
}
|
||||
}
|
||||
/// Construct a [`BlockRef`] from an instance of the underlying trait. It's expected
|
||||
/// that the [`Backend`] implementation will call this if it wants to track which blocks
|
||||
/// are potentially in use.
|
||||
pub fn new<P: BlockRefT>(hash: H, inner: P) -> Self {
|
||||
Self {
|
||||
hash,
|
||||
_pointer: Some(Arc::new(inner)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the hash of the referenced block.
|
||||
pub fn hash(&self) -> H
|
||||
where
|
||||
H: Copy,
|
||||
{
|
||||
self.hash
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that a [`Backend`] can implement to know when some block
|
||||
/// can be unpinned: when this is dropped, there are no remaining references
|
||||
/// to the block that it's associated with.
|
||||
pub trait BlockRefT: Send + Sync + 'static {}
|
||||
|
||||
/// A stream of some item.
|
||||
pub struct StreamOf<T>(Pin<Box<dyn Stream<Item = T> + Send + 'static>>);
|
||||
|
||||
impl<T> Stream for StreamOf<T> {
|
||||
type Item = T;
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
self.0.poll_next_unpin(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for StreamOf<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("StreamOf").field(&"<stream>").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> StreamOf<T> {
|
||||
/// Construct a new stream.
|
||||
pub fn new(inner: Pin<Box<dyn Stream<Item = T> + Send + 'static>>) -> Self {
|
||||
StreamOf(inner)
|
||||
}
|
||||
|
||||
/// Returns the next item in the stream. This is just a wrapper around
|
||||
/// [`StreamExt::next()`] so that you can avoid the extra import.
|
||||
pub async fn next(&mut self) -> Option<T> {
|
||||
StreamExt::next(self).await
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream of [`Result<Item, BackendError>`].
|
||||
pub type StreamOfResults<T> = StreamOf<Result<T, BackendError>>;
|
||||
|
||||
/// The status of the transaction.
|
||||
///
|
||||
/// If the status is [`TransactionStatus::InFinalizedBlock`], [`TransactionStatus::Error`],
|
||||
/// [`TransactionStatus::Invalid`] or [`TransactionStatus::Dropped`], then no future
|
||||
/// events will be emitted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TransactionStatus<Hash> {
|
||||
/// 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 {
|
||||
/// Block hash the transaction is in.
|
||||
hash: BlockRef<Hash>,
|
||||
},
|
||||
/// Transaction has been finalized by a finality-gadget, e.g GRANDPA
|
||||
InFinalizedBlock {
|
||||
/// Block hash the transaction is in.
|
||||
hash: BlockRef<Hash>,
|
||||
},
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
|
||||
/// A response from calls like [`Backend::storage_fetch_values`] or
|
||||
/// [`Backend::storage_fetch_descendant_values`].
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct StorageResponse {
|
||||
/// The key.
|
||||
pub key: Vec<u8>,
|
||||
/// The associated value.
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// 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 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.
|
||||
//!
|
||||
//! Specifically, the focus here is on the `archive` methods. These can only be used
|
||||
//! to interact with archive nodes, but are less restrictive than the `chainHead` methods
|
||||
//! in terms of the allowed operations.
|
||||
|
||||
mod storage_stream;
|
||||
|
||||
use crate::backend::{
|
||||
Backend, BlockRef, StorageResponse, StreamOf, StreamOfResults, TransactionStatus, utils::retry,
|
||||
};
|
||||
use crate::config::{Config, HashFor, RpcConfigFor};
|
||||
use crate::error::BackendError;
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use storage_stream::ArchiveStorageStream;
|
||||
use subxt_rpcs::RpcClient;
|
||||
use subxt_rpcs::methods::ChainHeadRpcMethods;
|
||||
use subxt_rpcs::methods::chain_head::{ArchiveCallResult, ArchiveStorageQuery, StorageQueryType};
|
||||
|
||||
/// The archive backend.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ArchiveBackend<T: Config> {
|
||||
// RPC methods we'll want to call:
|
||||
methods: ChainHeadRpcMethods<RpcConfigFor<T>>,
|
||||
}
|
||||
|
||||
impl<T: Config> ArchiveBackend<T> {
|
||||
/// Configure and construct an [`ArchiveBackend`] and the associated [`ChainHeadBackendDriver`].
|
||||
pub fn new(client: impl Into<RpcClient>) -> ArchiveBackend<T> {
|
||||
let methods = ChainHeadRpcMethods::new(client.into());
|
||||
|
||||
ArchiveBackend { methods }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Config> Backend<T> for ArchiveBackend<T> {
|
||||
async fn storage_fetch_values(
|
||||
&self,
|
||||
keys: Vec<Vec<u8>>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
|
||||
let queries = keys
|
||||
.into_iter()
|
||||
.map(|key| ArchiveStorageQuery {
|
||||
key: key,
|
||||
query_type: StorageQueryType::Value,
|
||||
pagination_start_key: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stream = ArchiveStorageStream::new(at, self.methods.clone(), queries)
|
||||
.map(|item| match item {
|
||||
Err(e) => Some(Err(e)),
|
||||
Ok(item) => item.value.map(|val| {
|
||||
Ok(StorageResponse {
|
||||
key: item.key.0,
|
||||
value: val.0,
|
||||
})
|
||||
}),
|
||||
})
|
||||
.filter_map(async |item| item);
|
||||
|
||||
Ok(StreamOf(Box::pin(stream)))
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_keys(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<Vec<u8>>, BackendError> {
|
||||
let queries = std::iter::once(ArchiveStorageQuery {
|
||||
key: key,
|
||||
// Just ask for the hash and then ignore it and return keys
|
||||
query_type: StorageQueryType::DescendantsHashes,
|
||||
pagination_start_key: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stream =
|
||||
ArchiveStorageStream::new(at, self.methods.clone(), queries).map(|item| match item {
|
||||
Err(e) => Err(e),
|
||||
Ok(item) => Ok(item.key.0),
|
||||
});
|
||||
|
||||
Ok(StreamOf(Box::pin(stream)))
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_values(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
|
||||
let queries = std::iter::once(ArchiveStorageQuery {
|
||||
key: key,
|
||||
query_type: StorageQueryType::DescendantsValues,
|
||||
pagination_start_key: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stream = ArchiveStorageStream::new(at, self.methods.clone(), queries)
|
||||
.map(|item| match item {
|
||||
Err(e) => Some(Err(e)),
|
||||
Ok(item) => item.value.map(|val| {
|
||||
Ok(StorageResponse {
|
||||
key: item.key.0,
|
||||
value: val.0,
|
||||
})
|
||||
}),
|
||||
})
|
||||
.filter_map(async |item| item);
|
||||
|
||||
Ok(StreamOf(Box::pin(stream)))
|
||||
}
|
||||
|
||||
async fn genesis_hash(&self) -> Result<HashFor<T>, BackendError> {
|
||||
retry(|| async {
|
||||
let hash = self.methods.archive_v1_genesis_hash().await?;
|
||||
Ok(hash)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_number_to_hash(
|
||||
&self,
|
||||
number: u64,
|
||||
) -> Result<Option<BlockRef<HashFor<T>>>, BackendError> {
|
||||
retry(|| async {
|
||||
let mut hashes = self
|
||||
.methods
|
||||
.archive_v1_hash_by_height(number as usize)
|
||||
.await?;
|
||||
if let (Some(hash), None) = (hashes.pop(), hashes.pop()) {
|
||||
// One hash; return it.
|
||||
Ok(Some(BlockRef::from_hash(hash)))
|
||||
} else {
|
||||
// More than one; return None.
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_header(&self, at: HashFor<T>) -> Result<Option<T::Header>, BackendError> {
|
||||
retry(|| async {
|
||||
let header = self.methods.archive_v1_header(at).await?;
|
||||
Ok(header)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_body(&self, at: HashFor<T>) -> Result<Option<Vec<Vec<u8>>>, BackendError> {
|
||||
retry(|| async {
|
||||
let Some(exts) = self.methods.archive_v1_body(at).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(exts.into_iter().map(|ext| ext.0).collect()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn latest_finalized_block_ref(&self) -> Result<BlockRef<HashFor<T>>, BackendError> {
|
||||
retry(|| async {
|
||||
let height = self.methods.archive_v1_finalized_height().await?;
|
||||
let mut hashes = self.methods.archive_v1_hash_by_height(height).await?;
|
||||
let Some(hash) = hashes.pop() else {
|
||||
return Err(BackendError::Other(
|
||||
"Multiple hashes not expected at a finalized height".into(),
|
||||
));
|
||||
};
|
||||
Ok(BlockRef::from_hash(hash))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stream_all_block_headers(
|
||||
&self,
|
||||
_hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
Err(BackendError::Other(
|
||||
"The archive backend cannot stream block headers".into(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn stream_best_block_headers(
|
||||
&self,
|
||||
_hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
Err(BackendError::Other(
|
||||
"The archive backend cannot stream block headers".into(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn stream_finalized_block_headers(
|
||||
&self,
|
||||
_hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
Err(BackendError::Other(
|
||||
"The archive backend cannot stream block headers".into(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn submit_transaction(
|
||||
&self,
|
||||
extrinsic: &[u8],
|
||||
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
|
||||
// This chainHead impl does not use chainHead_follow and so is suitable here too.
|
||||
super::chain_head::submit_transaction_ignoring_follow_events(extrinsic, &self.methods).await
|
||||
}
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<Vec<u8>, BackendError> {
|
||||
let res = self
|
||||
.methods
|
||||
.archive_v1_call(at, method, call_parameters.unwrap_or(&[]))
|
||||
.await?;
|
||||
match res {
|
||||
ArchiveCallResult::Success(bytes) => Ok(bytes.0),
|
||||
ArchiveCallResult::Error(e) => Err(BackendError::other(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> crate::backend::sealed::Sealed for ArchiveBackend<T> {}
|
||||
@@ -0,0 +1,183 @@
|
||||
use crate::config::{Config, HashFor, RpcConfigFor};
|
||||
use crate::error::BackendError;
|
||||
use futures::{FutureExt, Stream, StreamExt};
|
||||
use std::collections::VecDeque;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use subxt_rpcs::Error as RpcError;
|
||||
use subxt_rpcs::methods::ChainHeadRpcMethods;
|
||||
use subxt_rpcs::methods::chain_head::{
|
||||
ArchiveStorageEvent, ArchiveStorageEventItem, ArchiveStorageQuery, ArchiveStorageSubscription,
|
||||
};
|
||||
|
||||
pub struct ArchiveStorageStream<T: Config> {
|
||||
at: HashFor<T>,
|
||||
methods: ChainHeadRpcMethods<RpcConfigFor<T>>,
|
||||
query_queue: VecDeque<ArchiveStorageQuery<Vec<u8>>>,
|
||||
state: Option<StreamState<T>>,
|
||||
}
|
||||
|
||||
enum StreamState<T: Config> {
|
||||
GetSubscription {
|
||||
current_query: ArchiveStorageQuery<Vec<u8>>,
|
||||
sub_fut: Pin<
|
||||
Box<
|
||||
dyn Future<Output = Result<ArchiveStorageSubscription<HashFor<T>>, RpcError>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
>,
|
||||
>,
|
||||
},
|
||||
RunSubscription {
|
||||
current_query: ArchiveStorageQuery<Vec<u8>>,
|
||||
sub: ArchiveStorageSubscription<HashFor<T>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: Config> ArchiveStorageStream<T> {
|
||||
/// Fetch descendant keys.
|
||||
pub fn new(
|
||||
at: HashFor<T>,
|
||||
methods: ChainHeadRpcMethods<RpcConfigFor<T>>,
|
||||
query_queue: VecDeque<ArchiveStorageQuery<Vec<u8>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
at,
|
||||
methods,
|
||||
query_queue,
|
||||
state: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> std::marker::Unpin for ArchiveStorageStream<T> {}
|
||||
|
||||
impl<T: Config> Stream for ArchiveStorageStream<T> {
|
||||
type Item = Result<ArchiveStorageEventItem<HashFor<T>>, BackendError>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut();
|
||||
|
||||
loop {
|
||||
match this.state.take() {
|
||||
// No state yet so initialise!
|
||||
None => {
|
||||
// Nothing left; we're done.
|
||||
let Some(query) = this.query_queue.pop_front() else {
|
||||
return Poll::Ready(None);
|
||||
};
|
||||
|
||||
let at = this.at;
|
||||
let methods = this.methods.clone();
|
||||
let current_query = query.clone();
|
||||
let sub_fut = async move {
|
||||
let query = std::iter::once(ArchiveStorageQuery {
|
||||
key: query.key.as_ref(),
|
||||
query_type: query.query_type,
|
||||
pagination_start_key: query.pagination_start_key.as_deref(),
|
||||
});
|
||||
|
||||
methods.archive_v1_storage(at, query, None).await
|
||||
};
|
||||
|
||||
this.state = Some(StreamState::GetSubscription {
|
||||
current_query,
|
||||
sub_fut: Box::pin(sub_fut),
|
||||
});
|
||||
}
|
||||
// We're getting our subscription stream for the current query.
|
||||
Some(StreamState::GetSubscription {
|
||||
current_query,
|
||||
mut sub_fut,
|
||||
}) => {
|
||||
match sub_fut.poll_unpin(cx) {
|
||||
Poll::Ready(Ok(sub)) => {
|
||||
this.state = Some(StreamState::RunSubscription { current_query, sub });
|
||||
}
|
||||
Poll::Ready(Err(e)) => {
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
// Push the query back onto the queue to try again
|
||||
this.query_queue.push_front(current_query);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.state = None;
|
||||
return Poll::Ready(Some(Err(e.into())));
|
||||
}
|
||||
Poll::Pending => {
|
||||
this.state = Some(StreamState::GetSubscription {
|
||||
current_query,
|
||||
sub_fut,
|
||||
});
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Running the subscription and returning results.
|
||||
Some(StreamState::RunSubscription {
|
||||
current_query,
|
||||
mut sub,
|
||||
}) => {
|
||||
match sub.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Ok(val))) => {
|
||||
match val {
|
||||
ArchiveStorageEvent::Item(item) => {
|
||||
this.state = Some(StreamState::RunSubscription {
|
||||
current_query: ArchiveStorageQuery {
|
||||
key: current_query.key,
|
||||
query_type: current_query.query_type,
|
||||
// In the event of error, we resume from the last seen value.
|
||||
// At the time of writing, it's not clear if paginationStartKey
|
||||
// starts from the key itself or the first key after it:
|
||||
// https://github.com/paritytech/json-rpc-interface-spec/issues/176
|
||||
pagination_start_key: Some(item.key.0.clone()),
|
||||
},
|
||||
sub,
|
||||
});
|
||||
|
||||
// We treat `paginationStartKey` as being the key we want results to begin _after_.
|
||||
// So, if we see a value that's <= it, ignore the value.
|
||||
let ignore_this_value = current_query
|
||||
.pagination_start_key
|
||||
.as_ref()
|
||||
.is_some_and(|k| item.key.0.cmp(k).is_le());
|
||||
|
||||
if ignore_this_value {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Poll::Ready(Some(Ok(item)));
|
||||
}
|
||||
ArchiveStorageEvent::Error(e) => {
|
||||
this.state = None;
|
||||
return Poll::Ready(Some(Err(BackendError::other(e.error))));
|
||||
}
|
||||
ArchiveStorageEvent::Done => {
|
||||
this.state = None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => {
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
// Put the current query back into the queue and retry.
|
||||
// We've been keeping it uptodate as needed.
|
||||
this.query_queue.push_front(current_query);
|
||||
this.state = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
this.state = None;
|
||||
return Poll::Ready(Some(Err(e.into())));
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
this.state = None;
|
||||
continue;
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,11 @@
|
||||
// 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
|
||||
//! This module exposes 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.
|
||||
//! Specifically, the focus here is on the `chainHead` methods.
|
||||
|
||||
mod follow_stream;
|
||||
mod follow_stream_driver;
|
||||
@@ -18,10 +15,10 @@ mod storage_items;
|
||||
|
||||
use self::follow_stream_driver::FollowStreamFinalizedHeads;
|
||||
use crate::backend::{
|
||||
Backend, BlockRef, BlockRefT, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
|
||||
TransactionStatus, utils::retry,
|
||||
Backend, BlockRef, BlockRefT, StorageResponse, StreamOf, StreamOfResults, TransactionStatus,
|
||||
utils::retry,
|
||||
};
|
||||
use crate::config::{Config, Hash, HashFor};
|
||||
use crate::config::{Config, Hash, HashFor, RpcConfigFor};
|
||||
use crate::error::{BackendError, RpcError};
|
||||
use async_trait::async_trait;
|
||||
use follow_stream_driver::{FollowStreamDriver, FollowStreamDriverHandle};
|
||||
@@ -31,18 +28,11 @@ use std::collections::HashMap;
|
||||
use std::task::Poll;
|
||||
use storage_items::StorageItems;
|
||||
use subxt_rpcs::RpcClient;
|
||||
use subxt_rpcs::methods::ChainHeadRpcMethods;
|
||||
use subxt_rpcs::methods::chain_head::{
|
||||
FollowEvent, MethodResponse, RuntimeEvent, StorageQuery, StorageQueryType, StorageResultType,
|
||||
FollowEvent, MethodResponse, StorageQuery, StorageQueryType, StorageResultType,
|
||||
};
|
||||
|
||||
/// Re-export RPC types and methods from [`subxt_rpcs::methods::chain_head`].
|
||||
pub mod rpc_methods {
|
||||
pub use subxt_rpcs::methods::legacy::*;
|
||||
}
|
||||
|
||||
// Expose the RPC methods.
|
||||
pub use subxt_rpcs::methods::chain_head::ChainHeadRpcMethods;
|
||||
|
||||
/// Configure and build an [`ChainHeadBackend`].
|
||||
pub struct ChainHeadBackendBuilder<T> {
|
||||
max_block_life: usize,
|
||||
@@ -159,21 +149,10 @@ impl<T: Config> ChainHeadBackendBuilder<T> {
|
||||
/// - 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 {
|
||||
|
||||
super::utils::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 {
|
||||
@@ -210,7 +189,7 @@ impl<T: Config> Stream for ChainHeadBackendDriver<T> {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainHeadBackend<T: Config> {
|
||||
// RPC methods we'll want to call:
|
||||
methods: ChainHeadRpcMethods<T>,
|
||||
methods: ChainHeadRpcMethods<RpcConfigFor<T>>,
|
||||
// A handle to the chainHead_follow subscription:
|
||||
follow_handle: FollowStreamDriverHandle<HashFor<T>>,
|
||||
// How long to wait until giving up on transactions:
|
||||
@@ -285,7 +264,7 @@ impl<H: Hash + 'static> From<follow_stream_unpin::BlockRef<H>> for BlockRef<H> {
|
||||
impl<T: Config> super::sealed::Sealed for ChainHeadBackend<T> {}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
|
||||
impl<T: Config> Backend<T> for ChainHeadBackend<T> {
|
||||
async fn storage_fetch_values(
|
||||
&self,
|
||||
keys: Vec<Vec<u8>>,
|
||||
@@ -394,6 +373,15 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_number_to_hash(
|
||||
&self,
|
||||
_number: u64,
|
||||
) -> Result<Option<BlockRef<HashFor<T>>>, BackendError> {
|
||||
Err(BackendError::other(
|
||||
"The ChainHead V1 RPCs do not support obtaining a block hash from a number.",
|
||||
))
|
||||
}
|
||||
|
||||
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?;
|
||||
@@ -452,97 +440,6 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
|
||||
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,
|
||||
@@ -593,225 +490,6 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
|
||||
&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 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 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 {
|
||||
@@ -876,3 +554,222 @@ async fn get_subscription_id<H: Hash>(
|
||||
|
||||
Ok(sub_id)
|
||||
}
|
||||
|
||||
// Submit a transaction. This makes no attempt to sync with follow events,
|
||||
// This is used in the archive backend too.
|
||||
pub(crate) async fn submit_transaction_ignoring_follow_events<T: Config>(
|
||||
extrinsic: &[u8],
|
||||
methods: &ChainHeadRpcMethods<RpcConfigFor<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 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<RpcConfigFor<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: &'static str| Some(Err(BackendError::other(s)));
|
||||
|
||||
// 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 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)))
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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::config::{Config, HashFor, RpcConfigFor};
|
||||
use crate::error::BackendError;
|
||||
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
|
||||
use std::future::Future;
|
||||
@@ -103,7 +103,9 @@ impl<Hash> FollowStream<Hash> {
|
||||
}
|
||||
|
||||
/// Create a new [`FollowStream`] given the RPC methods.
|
||||
pub fn from_methods<T: Config>(methods: ChainHeadRpcMethods<T>) -> FollowStream<HashFor<T>> {
|
||||
pub fn from_methods<T: Config>(
|
||||
methods: ChainHeadRpcMethods<RpcConfigFor<T>>,
|
||||
) -> FollowStream<HashFor<T>> {
|
||||
FollowStream {
|
||||
stream_getter: Box::new(move || {
|
||||
let methods = methods.clone();
|
||||
@@ -112,9 +114,8 @@ impl<Hash> FollowStream<Hash> {
|
||||
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(),
|
||||
return Err(BackendError::other(
|
||||
"Subscription ID expected for chainHead_follow response, but not given",
|
||||
));
|
||||
};
|
||||
// Map stream errors into the higher level subxt one:
|
||||
@@ -311,7 +312,7 @@ pub mod test {
|
||||
Ok(FollowEvent::Stop),
|
||||
Ok(ev_new_block(1, 2)),
|
||||
// Nothing should be emitted after an error:
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
Ok(ev_new_block(2, 3)),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -537,7 +537,7 @@ mod test {
|
||||
Ok(ev_new_block(0, 1)),
|
||||
Ok(ev_best_block(1)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
10,
|
||||
@@ -580,7 +580,7 @@ mod test {
|
||||
Ok(ev_finalized([1], [])),
|
||||
Ok(ev_new_block(1, 2)),
|
||||
Ok(ev_new_block(2, 3)),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
10,
|
||||
@@ -630,7 +630,7 @@ mod test {
|
||||
Ok(ev_new_block(1, 2)),
|
||||
Ok(ev_new_block(2, 3)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
10,
|
||||
@@ -668,7 +668,7 @@ mod test {
|
||||
Ok(FollowEvent::Stop),
|
||||
Ok(ev_initialized(1)),
|
||||
Ok(ev_finalized([2], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
10,
|
||||
@@ -714,7 +714,7 @@ mod test {
|
||||
// Emulate that we missed some blocks.
|
||||
Ok(ev_initialized(13)),
|
||||
Ok(ev_finalized([14], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
10,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use super::ChainHeadRpcMethods;
|
||||
use super::follow_stream::FollowStream;
|
||||
use crate::config::{Config, Hash, HashFor};
|
||||
use crate::config::{Config, Hash, HashFor, RpcConfigFor};
|
||||
use crate::error::BackendError;
|
||||
use futures::stream::{FuturesUnordered, Stream, StreamExt};
|
||||
use subxt_rpcs::methods::chain_head::{
|
||||
@@ -275,7 +275,7 @@ impl<H: Hash> FollowStreamUnpin<H> {
|
||||
/// Create a new [`FollowStreamUnpin`] given the RPC methods.
|
||||
pub fn from_methods<T: Config>(
|
||||
follow_stream: FollowStream<HashFor<T>>,
|
||||
methods: ChainHeadRpcMethods<T>,
|
||||
methods: ChainHeadRpcMethods<RpcConfigFor<T>>,
|
||||
max_block_life: usize,
|
||||
) -> FollowStreamUnpin<HashFor<T>> {
|
||||
let unpin_method = Box::new(move |hash: HashFor<T>, sub_id: Arc<str>| {
|
||||
@@ -567,7 +567,7 @@ mod test {
|
||||
Ok(ev_new_block(0, 1)),
|
||||
Ok(ev_new_block(1, 2)),
|
||||
Ok(ev_new_block(2, 3)),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
10,
|
||||
@@ -593,7 +593,7 @@ mod test {
|
||||
[
|
||||
Ok(ev_initialized(0)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
3,
|
||||
@@ -624,7 +624,7 @@ mod test {
|
||||
Ok(ev_finalized([3], [])),
|
||||
Ok(ev_finalized([4], [])),
|
||||
Ok(ev_finalized([5], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
3,
|
||||
@@ -663,7 +663,7 @@ mod test {
|
||||
Ok(ev_new_block(1, 2)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Ok(ev_finalized([2], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
10,
|
||||
@@ -711,7 +711,7 @@ mod test {
|
||||
Ok(ev_finalized([1], [])),
|
||||
Ok(ev_finalized([2], [3])),
|
||||
Ok(ev_finalized([4], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
10,
|
||||
@@ -771,7 +771,7 @@ mod test {
|
||||
Ok(ev_best_block(1)),
|
||||
Ok(ev_finalized([1], [])),
|
||||
Ok(ev_finalized([2], [])),
|
||||
Err(BackendError::Other("ended".to_owned())),
|
||||
Err(BackendError::other("ended")),
|
||||
]
|
||||
},
|
||||
10,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use super::follow_stream_driver::FollowStreamDriverHandle;
|
||||
use super::follow_stream_unpin::BlockRef;
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::config::{Config, HashFor, RpcConfigFor};
|
||||
use crate::error::{BackendError, RpcError};
|
||||
use futures::{FutureExt, Stream, StreamExt};
|
||||
use std::collections::VecDeque;
|
||||
@@ -35,7 +35,7 @@ impl<T: Config> StorageItems<T> {
|
||||
queries: impl Iterator<Item = StorageQuery<&[u8]>>,
|
||||
at: HashFor<T>,
|
||||
follow_handle: &FollowStreamDriverHandle<HashFor<T>>,
|
||||
methods: ChainHeadRpcMethods<T>,
|
||||
methods: ChainHeadRpcMethods<RpcConfigFor<T>>,
|
||||
) -> Result<Self, BackendError> {
|
||||
let sub_id = super::get_subscription_id(follow_handle).await?;
|
||||
|
||||
@@ -157,7 +157,7 @@ impl<T: Config> Stream for StorageItems<T> {
|
||||
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))));
|
||||
return Poll::Ready(Some(Err(BackendError::other(err.error))));
|
||||
}
|
||||
_ => {
|
||||
// We don't care about this event; wait for the next.
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
// 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 backend implementation which will lookup the methods available
|
||||
//! to it from the RPC client and call methods accordingly.
|
||||
|
||||
use crate::backend::chain_head::ChainHeadBackendDriver;
|
||||
use crate::backend::{
|
||||
Backend, BlockRef, StorageResponse, StreamOfResults, TransactionStatus,
|
||||
archive::ArchiveBackend, chain_head::ChainHeadBackend, legacy::LegacyBackend,
|
||||
};
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::error::{BackendError, CombinedBackendError};
|
||||
use async_trait::async_trait;
|
||||
use futures::Stream;
|
||||
use futures::StreamExt;
|
||||
use std::task::Poll;
|
||||
use subxt_rpcs::RpcClient;
|
||||
|
||||
pub struct CombinedBackendBuilder<T: Config> {
|
||||
archive: BackendChoice<ArchiveBackend<T>>,
|
||||
chainhead: BackendChoice<ChainHeadBackend<T>>,
|
||||
legacy: BackendChoice<LegacyBackend<T>>,
|
||||
}
|
||||
|
||||
enum BackendChoice<V> {
|
||||
Use(V),
|
||||
DontUse,
|
||||
UseDefault,
|
||||
}
|
||||
|
||||
impl<T: Config> CombinedBackendBuilder<T> {
|
||||
/// Create a new [`CombinedBackendBuilder`].
|
||||
pub fn new() -> Self {
|
||||
CombinedBackendBuilder {
|
||||
archive: BackendChoice::UseDefault,
|
||||
chainhead: BackendChoice::UseDefault,
|
||||
legacy: BackendChoice::UseDefault,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the given [`ArchiveBackend`] where applicable.
|
||||
pub fn with_archive_backend(mut self, backend: ArchiveBackend<T>) -> Self {
|
||||
self.archive = BackendChoice::Use(backend);
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the given [`ChainHeadBackend`] where applicable.
|
||||
pub fn with_chainhead_backend(mut self, backend: ChainHeadBackend<T>) -> Self {
|
||||
self.chainhead = BackendChoice::Use(backend);
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the given [`LegacyBackend`] where applicable.
|
||||
pub fn with_legacy_backend(mut self, backend: LegacyBackend<T>) -> Self {
|
||||
self.legacy = BackendChoice::Use(backend);
|
||||
self
|
||||
}
|
||||
|
||||
/// Don't use any default backends; only use what is explicitly configured via
|
||||
/// [`CombinedBackendBuilder::with_archive_backend`],
|
||||
/// [`CombinedBackendBuilder::with_chainhead_backend`] and
|
||||
/// [`CombinedBackendBuilder::with_legacy_backend`].
|
||||
pub fn no_default_backends(mut self) -> Self {
|
||||
if matches!(self.legacy, BackendChoice::UseDefault) {
|
||||
self.legacy = BackendChoice::DontUse;
|
||||
}
|
||||
if matches!(self.archive, BackendChoice::UseDefault) {
|
||||
self.archive = BackendChoice::DontUse;
|
||||
}
|
||||
if matches!(self.chainhead, BackendChoice::UseDefault) {
|
||||
self.chainhead = BackendChoice::DontUse;
|
||||
}
|
||||
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 [`CombinedBackendBuilder::build_with_background_driver`] instead.
|
||||
pub async fn build(
|
||||
self,
|
||||
rpc_client: impl Into<RpcClient>,
|
||||
) -> Result<(CombinedBackend<T>, CombinedBackendDriver<T>), CombinedBackendError> {
|
||||
let rpc_client = rpc_client.into();
|
||||
|
||||
// What does the thing wer're talking to actually know about?
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Methods {
|
||||
methods: Vec<String>,
|
||||
}
|
||||
let methods: Methods = rpc_client
|
||||
.request("rpc_methods", subxt_rpcs::rpc_params![])
|
||||
.await
|
||||
.map_err(CombinedBackendError::CouldNotObtainRpcMethodList)?;
|
||||
let methods = methods.methods;
|
||||
|
||||
let has_archive_methods = methods.iter().any(|m| m.starts_with("archive_v1_"));
|
||||
let has_chainhead_methods = methods.iter().any(|m| m.starts_with("chainHead_v1"));
|
||||
|
||||
let mut combined_driver = CombinedBackendDriver {
|
||||
chainhead_driver: None,
|
||||
};
|
||||
|
||||
let archive = if has_archive_methods {
|
||||
match self.archive {
|
||||
BackendChoice::Use(b) => Some(b),
|
||||
BackendChoice::UseDefault => Some(ArchiveBackend::new(rpc_client.clone())),
|
||||
BackendChoice::DontUse => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let chainhead = if has_chainhead_methods {
|
||||
match self.chainhead {
|
||||
BackendChoice::Use(b) => Some(b),
|
||||
BackendChoice::UseDefault => {
|
||||
let (chainhead, chainhead_driver) =
|
||||
ChainHeadBackend::builder().build(rpc_client.clone());
|
||||
combined_driver.chainhead_driver = Some(chainhead_driver);
|
||||
Some(chainhead)
|
||||
}
|
||||
BackendChoice::DontUse => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let legacy = match self.legacy {
|
||||
BackendChoice::Use(b) => Some(b),
|
||||
BackendChoice::UseDefault => Some(LegacyBackend::builder().build(rpc_client.clone())),
|
||||
BackendChoice::DontUse => None,
|
||||
};
|
||||
|
||||
let combined = CombinedBackend {
|
||||
archive,
|
||||
chainhead,
|
||||
legacy,
|
||||
};
|
||||
|
||||
Ok((combined, combined_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")]
|
||||
pub async fn build_with_background_driver(
|
||||
self,
|
||||
client: impl Into<RpcClient>,
|
||||
) -> Result<CombinedBackend<T>, CombinedBackendError> {
|
||||
let (backend, mut driver) = self.build(client).await?;
|
||||
|
||||
super::utils::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", "combined backend was closed");
|
||||
});
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
}
|
||||
|
||||
/// Driver for the [`CombinedBackend`]. This needs to be polled to ensure
|
||||
/// that the [`CombinedBackend`] can make progress. It does not need polling
|
||||
/// if [`CombinedBackendDriver::needs_polling`] returns `false`.
|
||||
pub struct CombinedBackendDriver<T: Config> {
|
||||
chainhead_driver: Option<ChainHeadBackendDriver<T>>,
|
||||
}
|
||||
|
||||
impl<T: Config> CombinedBackendDriver<T> {
|
||||
pub fn needs_polling(&self) -> bool {
|
||||
self.chainhead_driver.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Stream for CombinedBackendDriver<T> {
|
||||
type Item = <ChainHeadBackendDriver<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>> {
|
||||
match &mut self.chainhead_driver {
|
||||
Some(driver) => driver.poll_next_unpin(cx),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A combined backend. This selects which RPC calls to use based on the `rpc_methods`
|
||||
/// available from the given RPC client we're given.
|
||||
pub struct CombinedBackend<T: Config> {
|
||||
archive: Option<ArchiveBackend<T>>,
|
||||
chainhead: Option<ChainHeadBackend<T>>,
|
||||
legacy: Option<LegacyBackend<T>>,
|
||||
}
|
||||
|
||||
impl<T: Config> CombinedBackend<T> {
|
||||
/// Configure and construct a [`CombinedBackend`].
|
||||
pub fn builder() -> CombinedBackendBuilder<T> {
|
||||
CombinedBackendBuilder::new()
|
||||
}
|
||||
|
||||
fn archive(&self) -> Option<&dyn Backend<T>> {
|
||||
self.archive.as_ref().map(|a| {
|
||||
let a: &dyn Backend<T> = a;
|
||||
a
|
||||
})
|
||||
}
|
||||
|
||||
fn chainhead(&self) -> Option<&dyn Backend<T>> {
|
||||
self.chainhead.as_ref().map(|a| {
|
||||
let a: &dyn Backend<T> = a;
|
||||
a
|
||||
})
|
||||
}
|
||||
|
||||
fn legacy(&self) -> Option<&dyn Backend<T>> {
|
||||
self.legacy.as_ref().map(|a| {
|
||||
let a: &dyn Backend<T> = a;
|
||||
a
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> super::sealed::Sealed for CombinedBackend<T> {}
|
||||
|
||||
// Our default behaviour:
|
||||
// - Try the archive backend first if it's available. Why? It has all block headers/bodies
|
||||
// etc so it's mroe likely to succeed than chainHead backend and give back things that won't
|
||||
// expire.
|
||||
// - If archive calls aren't available, fall back to the chainHead backend. Blocks given back
|
||||
// by this are more likely to expire.
|
||||
// - If neither exists / works, we fall back to the legacy methods. These have some limits on
|
||||
// what is available (often fewer limits than chainHead though) but tend to do the job. We'd
|
||||
// rather not use these as they are old and should go away, but until then they are a good
|
||||
// fallback.
|
||||
#[async_trait]
|
||||
impl<T: Config> Backend<T> for CombinedBackend<T> {
|
||||
async fn storage_fetch_values(
|
||||
&self,
|
||||
keys: Vec<Vec<u8>>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
|
||||
try_backends(
|
||||
&[self.archive(), self.chainhead(), self.legacy()],
|
||||
async |b: &dyn Backend<T>| b.storage_fetch_values(keys.clone(), at).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_keys(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<Vec<u8>>, BackendError> {
|
||||
try_backends(
|
||||
&[self.archive(), self.chainhead(), self.legacy()],
|
||||
async |b: &dyn Backend<T>| b.storage_fetch_descendant_keys(key.clone(), at).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn storage_fetch_descendant_values(
|
||||
&self,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
|
||||
try_backends(
|
||||
&[self.archive(), self.chainhead(), self.legacy()],
|
||||
async |b: &dyn Backend<T>| b.storage_fetch_descendant_values(key.clone(), at).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn genesis_hash(&self) -> Result<HashFor<T>, BackendError> {
|
||||
try_backends(
|
||||
&[self.archive(), self.chainhead(), self.legacy()],
|
||||
async |b: &dyn Backend<T>| b.genesis_hash().await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_number_to_hash(
|
||||
&self,
|
||||
number: u64,
|
||||
) -> Result<Option<BlockRef<HashFor<T>>>, BackendError> {
|
||||
try_backends(
|
||||
&[
|
||||
self.archive(),
|
||||
self.legacy(),
|
||||
// chainHead last as it cannot handle this request and will fail, so it's here
|
||||
// just to hand back a more relevant error in case the above two backends aren't
|
||||
// enabled or have some issue.
|
||||
self.chainhead(),
|
||||
],
|
||||
async |b: &dyn Backend<T>| b.block_number_to_hash(number).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_header(&self, at: HashFor<T>) -> Result<Option<T::Header>, BackendError> {
|
||||
try_backends(
|
||||
&[self.archive(), self.chainhead(), self.legacy()],
|
||||
async |b: &dyn Backend<T>| b.block_header(at).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_body(&self, at: HashFor<T>) -> Result<Option<Vec<Vec<u8>>>, BackendError> {
|
||||
try_backends(
|
||||
&[self.archive(), self.chainhead(), self.legacy()],
|
||||
async |b: &dyn Backend<T>| b.block_body(at).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn latest_finalized_block_ref(&self) -> Result<BlockRef<HashFor<T>>, BackendError> {
|
||||
try_backends(
|
||||
&[
|
||||
// Prioritize chainHead backend since it's streaming these things; save another call.
|
||||
self.chainhead(),
|
||||
self.archive(),
|
||||
self.legacy(),
|
||||
],
|
||||
async |b: &dyn Backend<T>| b.latest_finalized_block_ref().await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stream_all_block_headers(
|
||||
&self,
|
||||
hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
try_backends(
|
||||
&[
|
||||
// Ignore archive backend; it doesn't support this.
|
||||
self.chainhead(),
|
||||
self.legacy(),
|
||||
],
|
||||
async |b: &dyn Backend<T>| b.stream_all_block_headers(hasher.clone()).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stream_best_block_headers(
|
||||
&self,
|
||||
hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
try_backends(
|
||||
&[
|
||||
// Ignore archive backend; it doesn't support this.
|
||||
self.chainhead(),
|
||||
self.legacy(),
|
||||
],
|
||||
async |b: &dyn Backend<T>| b.stream_best_block_headers(hasher.clone()).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stream_finalized_block_headers(
|
||||
&self,
|
||||
hasher: T::Hasher,
|
||||
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
|
||||
try_backends(
|
||||
&[
|
||||
// Ignore archive backend; it doesn't support this.
|
||||
self.chainhead(),
|
||||
self.legacy(),
|
||||
],
|
||||
async |b: &dyn Backend<T>| b.stream_finalized_block_headers(hasher.clone()).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_transaction(
|
||||
&self,
|
||||
extrinsic: &[u8],
|
||||
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
|
||||
try_backends(
|
||||
&[
|
||||
// chainHead first as it does the same as the archive backend, but with better
|
||||
// guarantees around the block handed back being pinned & ready to access.
|
||||
self.chainhead(),
|
||||
self.legacy(),
|
||||
// archive last just incase chainHead & legacy fail or aren't provided for some
|
||||
// reason.
|
||||
self.archive(),
|
||||
],
|
||||
async |b: &dyn Backend<T>| b.submit_transaction(extrinsic).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
method: &str,
|
||||
call_parameters: Option<&[u8]>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<Vec<u8>, BackendError> {
|
||||
try_backends(
|
||||
&[self.archive(), self.chainhead(), self.legacy()],
|
||||
async |b: &dyn Backend<T>| b.call(method, call_parameters, at).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Call one backend after the other in the list until we get a successful result back.
|
||||
async fn try_backends<'s, 'b, T, Func, Fut, O>(
|
||||
backends: &'s [Option<&'b dyn Backend<T>>],
|
||||
mut f: Func,
|
||||
) -> Result<O, BackendError>
|
||||
where
|
||||
'b: 's,
|
||||
T: Config,
|
||||
Func: FnMut(&'b dyn Backend<T>) -> Fut,
|
||||
Fut: Future<Output = Result<O, BackendError>> + 'b,
|
||||
{
|
||||
static NO_AVAILABLE_BACKEND: &str =
|
||||
"None of the configured backends are capable of handling this request";
|
||||
let mut err = BackendError::other(NO_AVAILABLE_BACKEND);
|
||||
|
||||
for backend in backends.into_iter().filter_map(|b| *b) {
|
||||
match f(backend).await {
|
||||
Ok(res) => return Ok(res),
|
||||
Err(e) => err = e,
|
||||
}
|
||||
}
|
||||
|
||||
Err(err)
|
||||
}
|
||||
+46
-272
@@ -5,29 +5,22 @@
|
||||
//! This module exposes a legacy backend implementation, which relies
|
||||
//! on the legacy RPC API methods.
|
||||
|
||||
use self::rpc_methods::TransactionStatus as RpcTransactionStatus;
|
||||
mod descendant_streams;
|
||||
|
||||
use crate::backend::utils::{retry, retry_stream};
|
||||
use crate::backend::{
|
||||
Backend, BlockRef, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
|
||||
TransactionStatus,
|
||||
Backend, BlockRef, StorageResponse, StreamOf, StreamOfResults, TransactionStatus,
|
||||
};
|
||||
use crate::config::{Config, HashFor, Header};
|
||||
use crate::config::{Config, HashFor, Hasher, Header, RpcConfigFor};
|
||||
use crate::error::BackendError;
|
||||
use async_trait::async_trait;
|
||||
use codec::Encode;
|
||||
use descendant_streams::{StorageFetchDescendantKeysStream, StorageFetchDescendantValuesStream};
|
||||
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 futures::{Future, Stream, StreamExt, future, future::Either, stream};
|
||||
use subxt_rpcs::RpcClient;
|
||||
|
||||
/// Re-export legacy RPC types and methods from [`subxt_rpcs::methods::legacy`].
|
||||
pub mod rpc_methods {
|
||||
pub use subxt_rpcs::methods::legacy::*;
|
||||
}
|
||||
|
||||
// Expose the RPC methods.
|
||||
pub use rpc_methods::LegacyRpcMethods;
|
||||
use subxt_rpcs::methods::legacy::NumberOrHex;
|
||||
use subxt_rpcs::methods::legacy::{LegacyRpcMethods, TransactionStatus as RpcTransactionStatus};
|
||||
|
||||
/// Configure and build an [`LegacyBackend`].
|
||||
pub struct LegacyBackendBuilder<T> {
|
||||
@@ -72,7 +65,7 @@ impl<T: Config> LegacyBackendBuilder<T> {
|
||||
#[derive(Debug)]
|
||||
pub struct LegacyBackend<T> {
|
||||
storage_page_size: u32,
|
||||
methods: LegacyRpcMethods<T>,
|
||||
methods: LegacyRpcMethods<RpcConfigFor<T>>,
|
||||
}
|
||||
|
||||
impl<T> Clone for LegacyBackend<T> {
|
||||
@@ -94,7 +87,7 @@ impl<T: Config> LegacyBackend<T> {
|
||||
impl<T: Config> super::sealed::Sealed for LegacyBackend<T> {}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
impl<T: Config> Backend<T> for LegacyBackend<T> {
|
||||
async fn storage_fetch_values(
|
||||
&self,
|
||||
keys: Vec<Vec<u8>>,
|
||||
@@ -103,7 +96,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
fn get_entry<T: Config>(
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
methods: LegacyRpcMethods<T>,
|
||||
methods: LegacyRpcMethods<RpcConfigFor<T>>,
|
||||
) -> impl Future<Output = Result<Option<StorageResponse>, BackendError>> {
|
||||
retry(move || {
|
||||
let methods = methods.clone();
|
||||
@@ -137,15 +130,12 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<Vec<u8>>, BackendError> {
|
||||
let keys = StorageFetchDescendantKeysStream {
|
||||
at,
|
||||
let keys = StorageFetchDescendantKeysStream::new(
|
||||
self.methods.clone(),
|
||||
key,
|
||||
storage_page_size: self.storage_page_size,
|
||||
methods: self.methods.clone(),
|
||||
done: Default::default(),
|
||||
keys_fut: Default::default(),
|
||||
pagination_start_key: None,
|
||||
};
|
||||
at,
|
||||
self.storage_page_size,
|
||||
);
|
||||
|
||||
let keys = keys.flat_map(|keys| {
|
||||
match keys {
|
||||
@@ -168,21 +158,14 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
|
||||
let keys_stream = StorageFetchDescendantKeysStream {
|
||||
at,
|
||||
let values_stream = StorageFetchDescendantValuesStream::new(
|
||||
self.methods.clone(),
|
||||
key,
|
||||
storage_page_size: self.storage_page_size,
|
||||
methods: self.methods.clone(),
|
||||
done: Default::default(),
|
||||
keys_fut: Default::default(),
|
||||
pagination_start_key: None,
|
||||
};
|
||||
at,
|
||||
self.storage_page_size,
|
||||
);
|
||||
|
||||
Ok(StreamOf(Box::pin(StorageFetchDescendantValuesStream {
|
||||
keys: keys_stream,
|
||||
results_fut: None,
|
||||
results: Default::default(),
|
||||
})))
|
||||
Ok(StreamOf(Box::pin(values_stream)))
|
||||
}
|
||||
|
||||
async fn genesis_hash(&self) -> Result<HashFor<T>, BackendError> {
|
||||
@@ -193,6 +176,22 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_number_to_hash(
|
||||
&self,
|
||||
number: u64,
|
||||
) -> Result<Option<BlockRef<HashFor<T>>>, BackendError> {
|
||||
retry(|| async {
|
||||
let number_or_hash = NumberOrHex::Number(number);
|
||||
let hash = self
|
||||
.methods
|
||||
.chain_get_block_hash(Some(number_or_hash))
|
||||
.await?
|
||||
.map(BlockRef::from_hash);
|
||||
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?;
|
||||
@@ -221,56 +220,6 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
.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,
|
||||
@@ -278,11 +227,12 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
let methods = self.methods.clone();
|
||||
let retry_sub = retry_stream(move || {
|
||||
let methods = methods.clone();
|
||||
let hasher = hasher.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);
|
||||
let hash = hasher.hash(&h.encode());
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
@@ -302,11 +252,12 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
|
||||
let retry_sub = retry_stream(move || {
|
||||
let methods = methods.clone();
|
||||
let hasher = hasher.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);
|
||||
let hash = hasher.hash(&h.encode());
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
@@ -326,6 +277,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
|
||||
let retry_sub = retry_stream(move || {
|
||||
let this = this.clone();
|
||||
let hasher = hasher.clone();
|
||||
Box::pin(async move {
|
||||
let sub = this.methods.chain_subscribe_finalized_heads().await?;
|
||||
|
||||
@@ -345,7 +297,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
);
|
||||
let sub = sub.map(move |r| {
|
||||
r.map(|h| {
|
||||
let hash = h.hash_with(hasher);
|
||||
let hash = hasher.hash(&h.encode());
|
||||
(h, BlockRef::from_hash(hash))
|
||||
})
|
||||
});
|
||||
@@ -439,7 +391,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
|
||||
/// without notice in a patch release.
|
||||
#[doc(hidden)]
|
||||
pub fn subscribe_to_block_headers_filling_in_gaps<T, S, E>(
|
||||
methods: LegacyRpcMethods<T>,
|
||||
methods: LegacyRpcMethods<RpcConfigFor<T>>,
|
||||
sub: S,
|
||||
mut last_block_num: Option<u64>,
|
||||
) -> impl Stream<Item = Result<T::Header, BackendError>> + Send
|
||||
@@ -482,181 +434,3 @@ where
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
use super::LegacyRpcMethods;
|
||||
use crate::backend::StorageResponse;
|
||||
use crate::backend::utils::retry;
|
||||
use crate::config::{Config, HashFor, RpcConfigFor};
|
||||
use crate::error::BackendError;
|
||||
use futures::{Future, FutureExt, Stream, StreamExt};
|
||||
use std::collections::VecDeque;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
/// 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<RpcConfigFor<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> StorageFetchDescendantKeysStream<T> {
|
||||
/// Fetch descendant keys.
|
||||
pub fn new(
|
||||
methods: LegacyRpcMethods<RpcConfigFor<T>>,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
storage_page_size: u32,
|
||||
) -> Self {
|
||||
StorageFetchDescendantKeysStream {
|
||||
methods,
|
||||
key,
|
||||
at,
|
||||
storage_page_size,
|
||||
pagination_start_key: None,
|
||||
keys_fut: None,
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
match keys_fut.poll_unpin(cx) {
|
||||
Poll::Ready(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)));
|
||||
}
|
||||
Poll::Ready(Err(e)) => {
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
// Loop around and try again. No more keys_fut as it was taken,
|
||||
// so we'll ask for the keys again from the last good pagination_start_key.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Error getting keys? Return it.
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
}
|
||||
Poll::Pending => {
|
||||
this.keys_fut = Some(keys_fut);
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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_stream: StorageFetchDescendantKeysStream<T>,
|
||||
// Keys back from the stream which we are currently trying to fetch results for:
|
||||
keys: Vec<Vec<u8>>,
|
||||
// A future which will resolve to the resulting values:
|
||||
results_fut: Option<
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<Output = Result<Option<VecDeque<(Vec<u8>, Vec<u8>)>>, BackendError>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
// Once we get values back we put them here and hand them back one by one to the caller.
|
||||
results: VecDeque<(Vec<u8>, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl<T: Config> StorageFetchDescendantValuesStream<T> {
|
||||
/// Fetch descendant values.
|
||||
pub fn new(
|
||||
methods: LegacyRpcMethods<RpcConfigFor<T>>,
|
||||
key: Vec<u8>,
|
||||
at: HashFor<T>,
|
||||
storage_page_size: u32,
|
||||
) -> Self {
|
||||
StorageFetchDescendantValuesStream {
|
||||
keys_stream: StorageFetchDescendantKeysStream {
|
||||
methods,
|
||||
key,
|
||||
at,
|
||||
storage_page_size,
|
||||
pagination_start_key: None,
|
||||
keys_fut: None,
|
||||
done: false,
|
||||
},
|
||||
keys: Default::default(),
|
||||
results_fut: None,
|
||||
results: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))) => {
|
||||
// Clear keys once result comes back.
|
||||
this.keys = Vec::new();
|
||||
this.results = results;
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Ok(None)) => {
|
||||
// Clear keys once result comes back.
|
||||
this.keys = Vec::new();
|
||||
// But no results back for these keys we we just skip them.
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Err(e)) => {
|
||||
if e.is_disconnected_will_reconnect() {
|
||||
// Don't replace the `results_fut` since we got disconnected, and loop around.
|
||||
// This will cause us to try re-fetching results for the current keys.
|
||||
continue;
|
||||
}
|
||||
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
}
|
||||
Poll::Pending => {
|
||||
this.results_fut = Some(results_fut);
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have keys ready to fetch results for, then line up a results future to get them.
|
||||
// The keys stream handles disconnections internally for us.
|
||||
if !this.keys.is_empty() {
|
||||
let methods = this.keys_stream.methods.clone();
|
||||
let at = this.keys_stream.at;
|
||||
let keys = this.keys.clone();
|
||||
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;
|
||||
}
|
||||
|
||||
// We have no keys yet so wait for those first.
|
||||
match this.keys_stream.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Ok(keys))) => {
|
||||
this.keys = keys;
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+99
-94
@@ -1,89 +1,24 @@
|
||||
//! RPC utils.
|
||||
//! Backend 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
/// Spawn a task.
|
||||
///
|
||||
/// - On non-wasm targets, this will spawn a task via [`tokio::spawn`].
|
||||
/// - On wasm targets, this will spawn a task via [`wasm_bindgen_futures::spawn_local`].
|
||||
#[cfg(feature = "runtime")]
|
||||
pub(crate) 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;
|
||||
});
|
||||
}
|
||||
|
||||
/// Retry a future until it doesn't return a disconnected error.
|
||||
@@ -164,25 +99,95 @@ where
|
||||
/// }).await;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn retry_stream<F, R>(sub_stream: F) -> Result<StreamOfResults<R>, BackendError>
|
||||
pub async fn retry_stream<F, Fut, R>(get_stream: F) -> Result<StreamOfResults<R>, BackendError>
|
||||
where
|
||||
F: FnMut() -> ResubscribeFuture<R> + Send + 'static + Clone,
|
||||
F: Clone + Send + 'static + FnMut() -> Fut,
|
||||
Fut: Future<Output = Result<StreamOfResults<R>, BackendError>> + Send,
|
||||
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()
|
||||
});
|
||||
// This returns the stream. On disconnect this is called again.
|
||||
let get_stream_with_retry = move || {
|
||||
let get_stream = get_stream.clone();
|
||||
async move { retry(get_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,
|
||||
state: RetrySubscriptionState::Init,
|
||||
resubscribe: get_stream_with_retry,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Retry subscription.
|
||||
struct RetrySubscription<F, R, T> {
|
||||
resubscribe: F,
|
||||
state: RetrySubscriptionState<R, T>,
|
||||
}
|
||||
|
||||
enum RetrySubscriptionState<R, T> {
|
||||
Init,
|
||||
Pending(R),
|
||||
Stream(StreamOfResults<T>),
|
||||
Done,
|
||||
}
|
||||
|
||||
impl<F, R, T> std::marker::Unpin for RetrySubscription<F, R, T> {}
|
||||
|
||||
impl<F, R, T> Stream for RetrySubscription<F, R, T>
|
||||
where
|
||||
F: FnMut() -> R,
|
||||
R: Future<Output = Result<StreamOfResults<T>, BackendError>> + Unpin,
|
||||
{
|
||||
type Item = Result<T, BackendError>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
match &mut self.state {
|
||||
RetrySubscriptionState::Init => {
|
||||
self.state = RetrySubscriptionState::Pending((self.resubscribe)());
|
||||
}
|
||||
RetrySubscriptionState::Stream(s) => match s.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Err(err))) => {
|
||||
if err.is_disconnected_will_reconnect() {
|
||||
self.state = RetrySubscriptionState::Init;
|
||||
}
|
||||
return Poll::Ready(Some(Err(err)));
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
self.state = RetrySubscriptionState::Done;
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
Poll::Ready(Some(Ok(val))) => {
|
||||
return Poll::Ready(Some(Ok(val)));
|
||||
}
|
||||
Poll::Pending => {
|
||||
return Poll::Pending;
|
||||
}
|
||||
},
|
||||
RetrySubscriptionState::Pending(fut) => match fut.poll_unpin(cx) {
|
||||
Poll::Ready(Err(err)) => {
|
||||
if err.is_disconnected_will_reconnect() {
|
||||
self.state = RetrySubscriptionState::Init;
|
||||
}
|
||||
return Poll::Ready(Some(Err(err)));
|
||||
}
|
||||
Poll::Ready(Ok(stream)) => {
|
||||
self.state = RetrySubscriptionState::Stream(stream);
|
||||
continue;
|
||||
}
|
||||
Poll::Pending => {
|
||||
return Poll::Pending;
|
||||
}
|
||||
},
|
||||
RetrySubscriptionState::Done => return Poll::Ready(None),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -193,7 +198,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn custom_err() -> BackendError {
|
||||
BackendError::Other(String::new())
|
||||
BackendError::other("")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -233,7 +238,7 @@ mod tests {
|
||||
});
|
||||
|
||||
let retry_stream = RetrySubscription {
|
||||
state: Some(PendingOrStream::Stream(StreamOf::new(Box::pin(stream)))),
|
||||
state: RetrySubscriptionState::Stream(StreamOf::new(Box::pin(stream))),
|
||||
resubscribe,
|
||||
};
|
||||
|
||||
@@ -250,7 +255,7 @@ mod tests {
|
||||
let resubscribe = Box::new(|| async move { Err(custom_err()) }.boxed());
|
||||
|
||||
let retry_stream = RetrySubscription {
|
||||
state: Some(PendingOrStream::Stream(StreamOf::new(Box::pin(stream)))),
|
||||
state: RetrySubscriptionState::Stream(StreamOf::new(Box::pin(stream))),
|
||||
resubscribe,
|
||||
};
|
||||
|
||||
@@ -263,7 +268,7 @@ mod tests {
|
||||
let resubscribe = Box::new(|| async move { Err(custom_err()) }.boxed());
|
||||
|
||||
let retry_stream = RetrySubscription {
|
||||
state: Some(PendingOrStream::Stream(StreamOf::new(Box::pin(stream)))),
|
||||
state: RetrySubscriptionState::Stream(StreamOf::new(Box::pin(stream))),
|
||||
resubscribe,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
// 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)))
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
// 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 subxt_core::blocks::{ExtrinsicDetails as CoreExtrinsicDetails, Extrinsics as CoreExtrinsics};
|
||||
|
||||
// Re-export anything that's directly returned/used in the APIs below.
|
||||
pub use 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 [`subxt_core::blocks::Extrinsics::len()`].
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
/// See [`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 [`subxt_core::blocks::ExtrinsicDetails::hash()`].
|
||||
pub fn hash(&self) -> HashFor<T> {
|
||||
self.inner.hash()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::is_signed()`].
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.inner.is_signed()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::index()`].
|
||||
pub fn index(&self) -> u32 {
|
||||
self.inner.index()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::bytes()`].
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
self.inner.bytes()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::call_bytes()`].
|
||||
pub fn call_bytes(&self) -> &[u8] {
|
||||
self.inner.call_bytes()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::field_bytes()`].
|
||||
pub fn field_bytes(&self) -> &[u8] {
|
||||
self.inner.field_bytes()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::address_bytes()`].
|
||||
pub fn address_bytes(&self) -> Option<&[u8]> {
|
||||
self.inner.address_bytes()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::signature_bytes()`].
|
||||
pub fn signature_bytes(&self) -> Option<&[u8]> {
|
||||
self.inner.signature_bytes()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::transaction_extensions_bytes()`].
|
||||
pub fn transaction_extensions_bytes(&self) -> Option<&[u8]> {
|
||||
self.inner.transaction_extensions_bytes()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::transaction_extensions()`].
|
||||
pub fn transaction_extensions(&self) -> Option<ExtrinsicTransactionExtensions<'_, T>> {
|
||||
self.inner.transaction_extensions()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::pallet_index()`].
|
||||
pub fn pallet_index(&self) -> u8 {
|
||||
self.inner.pallet_index()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::call_index()`].
|
||||
pub fn call_index(&self) -> u8 {
|
||||
self.inner.call_index()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::pallet_name()`].
|
||||
pub fn pallet_name(&self) -> &str {
|
||||
self.inner.pallet_name()
|
||||
}
|
||||
|
||||
/// See [`subxt_core::blocks::ExtrinsicDetails::call_name()`].
|
||||
pub fn call_name(&self) -> &str {
|
||||
self.inner.call_name()
|
||||
}
|
||||
|
||||
/// See [`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 [`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 [`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())
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,108 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,52 +0,0 @@
|
||||
// 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")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -1,66 +0,0 @@
|
||||
// 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.
|
||||
//!
|
||||
@@ -1,166 +0,0 @@
|
||||
//! # 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")]
|
||||
//! ```
|
||||
@@ -1,14 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,103 +0,0 @@
|
||||
// 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()).
|
||||
//!
|
||||
@@ -1,55 +0,0 @@
|
||||
// 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")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -1,69 +0,0 @@
|
||||
// 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)?;
|
||||
//! ```
|
||||
//!
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -1,51 +0,0 @@
|
||||
// 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")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -1,27 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,22 +0,0 @@
|
||||
// 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 [`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 [`subxt_rpcs`] or [`crate::ext::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")]
|
||||
//! ```
|
||||
@@ -1,80 +0,0 @@
|
||||
// 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")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -1,82 +0,0 @@
|
||||
// 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")]
|
||||
//! ```
|
||||
//!
|
||||
@@ -1,202 +0,0 @@
|
||||
// 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 `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 `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 `subxt_signer` crate:
|
||||
//!
|
||||
//! ```rust,standalone_crate
|
||||
//! use subxt::config::PolkadotConfig;
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! use 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 = 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.
|
||||
//!
|
||||
@@ -0,0 +1,108 @@
|
||||
mod offline_client;
|
||||
mod online_client;
|
||||
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::constants::ConstantsClient;
|
||||
use crate::custom_values::CustomValuesClient;
|
||||
use crate::events::EventsClient;
|
||||
use crate::extrinsics::ExtrinsicsClient;
|
||||
use crate::runtime_apis::RuntimeApisClient;
|
||||
use crate::storage::StorageClient;
|
||||
use crate::transactions::TransactionsClient;
|
||||
use crate::view_functions::ViewFunctionsClient;
|
||||
use core::marker::PhantomData;
|
||||
use subxt_metadata::Metadata;
|
||||
|
||||
pub use offline_client::{OfflineClient, OfflineClientAtBlock, OfflineClientAtBlockT};
|
||||
pub use online_client::{
|
||||
BlockNumberOrRef, OnlineClient, OnlineClientAtBlock, OnlineClientAtBlockT,
|
||||
};
|
||||
|
||||
/// This represents a client at a specific block number.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClientAtBlock<T, Client> {
|
||||
pub(crate) client: Client,
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, Client> ClientAtBlock<T, Client> {
|
||||
/// Construct a new client at some block.
|
||||
pub(crate) fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> ClientAtBlock<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OfflineClientAtBlockT<T>,
|
||||
{
|
||||
/// Construct and submit transactions. This is a
|
||||
/// shorthand to [`Self::transactions()`].
|
||||
pub fn tx(&self) -> TransactionsClient<'_, T, Client> {
|
||||
TransactionsClient::new(&self.client)
|
||||
}
|
||||
|
||||
/// Construct and submit transactions.
|
||||
pub fn transactions(&self) -> TransactionsClient<'_, T, Client> {
|
||||
TransactionsClient::new(&self.client)
|
||||
}
|
||||
|
||||
/// Access storage at this block.
|
||||
pub fn storage(&self) -> StorageClient<'_, T, Client> {
|
||||
StorageClient::new(&self.client)
|
||||
}
|
||||
|
||||
/// Access constants at this block.
|
||||
pub fn constants(&self) -> ConstantsClient<'_, T, Client> {
|
||||
ConstantsClient::new(&self.client)
|
||||
}
|
||||
|
||||
/// Access custom values at this block.
|
||||
pub fn custom_values(&self) -> CustomValuesClient<'_, T, Client> {
|
||||
CustomValuesClient::new(&self.client)
|
||||
}
|
||||
|
||||
/// Work with the extrinsics in this block.
|
||||
pub fn extrinsics(&self) -> ExtrinsicsClient<'_, T, Client> {
|
||||
ExtrinsicsClient::new(&self.client)
|
||||
}
|
||||
|
||||
/// Work with the events at this block.
|
||||
pub fn events(&self) -> EventsClient<'_, T, Client> {
|
||||
EventsClient::new(&self.client)
|
||||
}
|
||||
|
||||
/// Access runtime APIs at this block.
|
||||
pub fn runtime_apis(&self) -> RuntimeApisClient<'_, T, Client> {
|
||||
RuntimeApisClient::new(&self.client)
|
||||
}
|
||||
|
||||
pub fn view_functions(&self) -> ViewFunctionsClient<'_, T, Client> {
|
||||
ViewFunctionsClient::new(&self.client)
|
||||
}
|
||||
|
||||
/// Obtain a reference to the metadata.
|
||||
pub fn metadata_ref(&self) -> &Metadata {
|
||||
self.client.metadata_ref()
|
||||
}
|
||||
|
||||
/// The current block number.
|
||||
pub fn block_number(&self) -> u64 {
|
||||
self.client.block_number()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Client> ClientAtBlock<T, Client>
|
||||
where
|
||||
T: Config,
|
||||
Client: OnlineClientAtBlockT<T>,
|
||||
{
|
||||
/// The current block hash.
|
||||
pub fn block_hash(&self) -> HashFor<T> {
|
||||
self.client.block_hash()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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 subxt_core::client::{ClientState, RuntimeVersion};
|
||||
@@ -1,203 +1,106 @@
|
||||
// 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::client::ClientAtBlock;
|
||||
use crate::config::{Config, HashFor, Hasher};
|
||||
use crate::error::OfflineClientAtBlockError;
|
||||
use crate::metadata::{ArcMetadata, Metadata};
|
||||
|
||||
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 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 [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)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OfflineClient<T: Config> {
|
||||
inner: Arc<ClientState<T>>,
|
||||
hasher: T::Hasher,
|
||||
/// The configuration for this client.
|
||||
config: T,
|
||||
}
|
||||
|
||||
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 subxt_core::config::Hasher>::new(&metadata);
|
||||
/// Create a new [`OfflineClient`] with the given configuration.
|
||||
pub fn new(config: T) -> Self {
|
||||
OfflineClient { config }
|
||||
}
|
||||
|
||||
OfflineClient {
|
||||
/// Pick the block height at which to operate. This references data from the
|
||||
/// [`OfflineClient`] it's called on, and so cannot outlive it.
|
||||
pub fn at_block(
|
||||
&self,
|
||||
block_number: impl Into<u64>,
|
||||
) -> Result<ClientAtBlock<T, OfflineClientAtBlock<T>>, OfflineClientAtBlockError> {
|
||||
let block_number = block_number.into();
|
||||
let (spec_version, transaction_version) = self
|
||||
.config
|
||||
.spec_and_transaction_version_for_block_number(block_number)
|
||||
.ok_or(OfflineClientAtBlockError::SpecVersionNotFound { block_number })?;
|
||||
|
||||
let metadata = self
|
||||
.config
|
||||
.metadata_for_spec_version(spec_version)
|
||||
.ok_or(OfflineClientAtBlockError::MetadataNotFound { spec_version })?;
|
||||
|
||||
let genesis_hash = self.config.genesis_hash();
|
||||
|
||||
let hasher = <T::Hasher as Hasher>::new(&metadata);
|
||||
|
||||
let offline_client_at_block = OfflineClientAtBlock {
|
||||
metadata,
|
||||
block_number,
|
||||
genesis_hash,
|
||||
spec_version,
|
||||
hasher,
|
||||
inner: Arc::new(ClientState {
|
||||
genesis_hash,
|
||||
runtime_version,
|
||||
metadata,
|
||||
}),
|
||||
}
|
||||
}
|
||||
transaction_version,
|
||||
};
|
||||
|
||||
/// 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)
|
||||
Ok(ClientAtBlock::new(offline_client_at_block))
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct OfflineClientAtBlock<T: Config> {
|
||||
metadata: ArcMetadata,
|
||||
block_number: u64,
|
||||
genesis_hash: Option<HashFor<T>>,
|
||||
spec_version: u32,
|
||||
hasher: T::Hasher,
|
||||
transaction_version: u32,
|
||||
}
|
||||
|
||||
// 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()
|
||||
/// This represents an offline-only client at a specific block.
|
||||
#[doc(hidden)]
|
||||
pub trait OfflineClientAtBlockT<T: Config>: Clone {
|
||||
/// Get a reference to the metadata appropriate for this block.
|
||||
fn metadata_ref(&self) -> &Metadata;
|
||||
/// Get a clone of the metadata appropriate for this block.
|
||||
fn metadata(&self) -> ArcMetadata;
|
||||
/// The block number we're operating at.
|
||||
fn block_number(&self) -> u64;
|
||||
/// Return the genesis hash for the chain if it is known.
|
||||
fn genesis_hash(&self) -> Option<HashFor<T>>;
|
||||
/// The spec version at the current block.
|
||||
fn spec_version(&self) -> u32;
|
||||
/// Return a hasher that works at the current block.
|
||||
fn hasher(&self) -> &T::Hasher;
|
||||
/// The transaction version at the current block.
|
||||
///
|
||||
/// Note: This is _not_ the same as the transaction version that
|
||||
/// is encoded at the beginning of transactions (ie 4 or 5).
|
||||
fn transaction_version(&self) -> u32;
|
||||
}
|
||||
|
||||
impl<T: Config> OfflineClientAtBlockT<T> for OfflineClientAtBlock<T> {
|
||||
fn metadata_ref(&self) -> &Metadata {
|
||||
&self.metadata
|
||||
}
|
||||
fn metadata(&self) -> ArcMetadata {
|
||||
self.metadata.clone()
|
||||
}
|
||||
fn block_number(&self) -> u64 {
|
||||
self.block_number
|
||||
}
|
||||
fn genesis_hash(&self) -> Option<HashFor<T>> {
|
||||
self.genesis_hash
|
||||
}
|
||||
fn spec_version(&self) -> u32 {
|
||||
self.spec_version
|
||||
}
|
||||
fn transaction_version(&self) -> u32 {
|
||||
self.transaction_version
|
||||
}
|
||||
fn hasher(&self) -> &T::Hasher {
|
||||
&self.hasher
|
||||
}
|
||||
}
|
||||
|
||||
+520
-507
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
use crate::backend::BlockRef;
|
||||
use crate::config::{Config, HashFor, Hasher};
|
||||
|
||||
/// This represents either a block number or a reference
|
||||
/// to a block, which is essentially a block hash.
|
||||
pub enum BlockNumberOrRef<T: Config> {
|
||||
/// A block number.
|
||||
Number(u64),
|
||||
/// A block ref / hash.
|
||||
BlockRef(BlockRef<HashFor<T>>),
|
||||
}
|
||||
|
||||
impl<T: Config> From<u32> for BlockNumberOrRef<T> {
|
||||
fn from(value: u32) -> Self {
|
||||
BlockNumberOrRef::Number(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> From<u64> for BlockNumberOrRef<T> {
|
||||
fn from(value: u64) -> Self {
|
||||
BlockNumberOrRef::Number(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> From<BlockRef<HashFor<T>>> for BlockNumberOrRef<T> {
|
||||
fn from(block_ref: BlockRef<HashFor<T>>) -> Self {
|
||||
BlockNumberOrRef::BlockRef(block_ref)
|
||||
}
|
||||
}
|
||||
|
||||
// Ideally we'd have `impl From<HashFor<T>> for BlockNumberOrRef<T>` but since our config
|
||||
// could set _any_ hash type, this boils down to `impl From<H> for ..` which is too general.
|
||||
// Thus, we target our current concrete hash type.
|
||||
impl<T: Config> From<crate::config::substrate::H256> for BlockNumberOrRef<T>
|
||||
where
|
||||
<T::Hasher as Hasher>::Hash: From<crate::config::substrate::H256>,
|
||||
{
|
||||
fn from(hash: crate::config::substrate::H256) -> Self {
|
||||
BlockNumberOrRef::BlockRef(BlockRef::from_hash(hash.into()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use crate::backend::{BlockRef, StreamOfResults};
|
||||
use crate::client::{ClientAtBlock, OnlineClient, OnlineClientAtBlock};
|
||||
use crate::config::{Config, HashFor, Header};
|
||||
use crate::error::{BlocksError, OnlineClientAtBlockError};
|
||||
use futures::{Stream, StreamExt};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
/// A stream of blocks.
|
||||
pub struct Blocks<T: Config> {
|
||||
client: OnlineClient<T>,
|
||||
stream: StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>,
|
||||
}
|
||||
|
||||
impl<T: Config> Blocks<T> {
|
||||
pub(crate) fn from_headers_stream(
|
||||
client: OnlineClient<T>,
|
||||
stream: StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>,
|
||||
) -> Self {
|
||||
Blocks { client, stream }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Stream for Blocks<T> {
|
||||
type Item = Result<Block<T>, BlocksError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let item = match self.stream.poll_next_unpin(cx) {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Ready(Some(item)) => item,
|
||||
};
|
||||
|
||||
let res = match item {
|
||||
Ok((block_header, block_ref)) => Ok(Block {
|
||||
block_ref,
|
||||
block_header,
|
||||
client: self.client.clone(),
|
||||
}),
|
||||
Err(e) => Err(BlocksError::CannotGetBlockHeader(e)),
|
||||
};
|
||||
|
||||
Poll::Ready(Some(res))
|
||||
}
|
||||
}
|
||||
|
||||
/// A block from the stream of blocks.
|
||||
pub struct Block<T: Config> {
|
||||
block_ref: BlockRef<HashFor<T>>,
|
||||
block_header: T::Header,
|
||||
client: OnlineClient<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Block<T> {
|
||||
/// The block hash
|
||||
pub fn hash(&self) -> HashFor<T> {
|
||||
self.block_ref.hash()
|
||||
}
|
||||
|
||||
/// The block number.
|
||||
pub fn number(&self) -> u64 {
|
||||
self.block_header.number()
|
||||
}
|
||||
|
||||
/// The block header.
|
||||
pub fn header(&self) -> &T::Header {
|
||||
&self.block_header
|
||||
}
|
||||
|
||||
/// Instantiate a client at this block.
|
||||
pub async fn client(
|
||||
&self,
|
||||
) -> Result<ClientAtBlock<T, OnlineClientAtBlock<T>>, OnlineClientAtBlockError> {
|
||||
self.client.at_block(self.block_ref.clone()).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// 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.
|
||||
|
||||
//! 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.
|
||||
|
||||
mod default_extrinsic_params;
|
||||
|
||||
pub mod extrinsic_params;
|
||||
pub mod polkadot;
|
||||
pub mod substrate;
|
||||
pub mod transaction_extensions;
|
||||
|
||||
use crate::metadata::{ArcMetadata, Metadata};
|
||||
use codec::{Decode, Encode};
|
||||
use core::fmt::Debug;
|
||||
use scale_decode::DecodeAsType;
|
||||
use scale_encode::EncodeAsType;
|
||||
use scale_info_legacy::TypeRegistrySet;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::{fmt::Display, marker::PhantomData};
|
||||
use subxt_rpcs::RpcConfig;
|
||||
|
||||
pub use default_extrinsic_params::{DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder};
|
||||
pub use extrinsic_params::{ClientState, ExtrinsicParams, ExtrinsicParamsEncoder};
|
||||
pub use polkadot::{PolkadotConfig, PolkadotExtrinsicParams, PolkadotExtrinsicParamsBuilder};
|
||||
pub use substrate::{SubstrateConfig, SubstrateExtrinsicParams, SubstrateExtrinsicParamsBuilder};
|
||||
pub use transaction_extensions::TransactionExtension;
|
||||
|
||||
/// Configuration for a given chain and the runtimes within. This consists of the
|
||||
/// type information needed to work at the head of the chain (namely submitting
|
||||
/// transactions), as well as functionality which we might wish to customize for a
|
||||
/// given chain.
|
||||
pub trait Config: Clone + Debug + Sized + Send + Sync + 'static {
|
||||
/// The account ID type; required for constructing extrinsics.
|
||||
type AccountId: Debug + Clone + Encode + Decode + Serialize + Send;
|
||||
|
||||
/// The address type; required for constructing extrinsics.
|
||||
type Address: Debug + Encode + From<Self::AccountId>;
|
||||
|
||||
/// The signature type.
|
||||
type Signature: Debug + Clone + Encode + Decode + Send;
|
||||
|
||||
/// The block header.
|
||||
type Header: Header;
|
||||
|
||||
/// This type defines the extrinsic extra and additional parameters.
|
||||
type ExtrinsicParams: ExtrinsicParams<Self>;
|
||||
|
||||
/// This is used to identify an asset in the `ChargeAssetTxPayment` signed extension.
|
||||
type AssetId: Debug + Clone + Encode + DecodeAsType + EncodeAsType + Send;
|
||||
|
||||
/// The hashing system (algorithm) being used in the runtime (e.g. Blake2).
|
||||
/// This is created on demand with the relevant metadata for a given block, and
|
||||
/// can then be used to hash things at that block.
|
||||
type Hasher: Hasher;
|
||||
|
||||
/// The starting hash for the chain we're connecting to. This is required for constructing transactions.
|
||||
///
|
||||
/// If not provided by the config implementation, it will be obtained from the chain in the case of the
|
||||
/// [`crate::client::OnlineClient`]. It must be provided to construct transactions via the
|
||||
/// [`crate::client::OfflineClient`], else an error will be returned.
|
||||
fn genesis_hash(&self) -> Option<HashFor<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Return a tuple of the spec version and then transaction version for a given block number, if available.
|
||||
///
|
||||
/// The [`crate::client::OnlineClient`] will look this up on chain if it's not available here,
|
||||
/// but the [`crate::client::OfflineClient`] will error if this is not available for the required block number.
|
||||
fn spec_and_transaction_version_for_block_number(
|
||||
&self,
|
||||
_block_number: u64,
|
||||
) -> Option<(u32, u32)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the metadata for a given spec version, if available.
|
||||
///
|
||||
/// The [`crate::client::OnlineClient`] will look this up on chain if it's not available here, and then
|
||||
/// call [`Config::set_metadata_for_spec_version`] to give the configuration the opportunity to cache it.
|
||||
/// The [`crate::client::OfflineClient`] will error if this is not available for the required spec version.
|
||||
fn metadata_for_spec_version(&self, _spec_version: u32) -> Option<ArcMetadata> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Set some metadata for a given spec version. the [`crate::client::OnlineClient`] will call this if it has
|
||||
/// to retrieve metadata from the chain, to give this the opportunity to cache it. The configuration can
|
||||
/// do nothing if it prefers.
|
||||
fn set_metadata_for_spec_version(&self, _spec_version: u32, _metadata: ArcMetadata) {}
|
||||
|
||||
/// Return legacy types (ie types to use with Runtimes that return pre-V14 metadata) for a given spec version.
|
||||
/// If this returns `None`, [`subxt`] will return an error if type definitions are needed to access some older
|
||||
/// block.
|
||||
///
|
||||
/// This doesn't need to live for long; it will be used to translate any older metadata returned from the node
|
||||
/// into our [`Metadata`] type, which will then be used.
|
||||
fn legacy_types_for_spec_version<'this>(
|
||||
&'this self,
|
||||
_spec_version: u32,
|
||||
) -> Option<TypeRegistrySet<'this>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// `RpcConfigFor<Config>` can be used anywhere which requires an implementation of [`subxt_rpcs::RpcConfig`].
|
||||
/// This is only needed at the type level, and so there is no way to construct this.
|
||||
pub struct RpcConfigFor<T> {
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> RpcConfig for RpcConfigFor<T> {
|
||||
type Hash = HashFor<T>;
|
||||
type Header = T::Header;
|
||||
type AccountId = T::AccountId;
|
||||
}
|
||||
|
||||
/// Given some [`Config`], this returns the type of hash used.
|
||||
pub type HashFor<T> = <<T as Config>::Hasher as Hasher>::Hash;
|
||||
|
||||
/// given some [`Config`], this return the other params needed for its `ExtrinsicParams`.
|
||||
pub type ParamsFor<T> = <<T as Config>::ExtrinsicParams as ExtrinsicParams<T>>::Params;
|
||||
|
||||
/// Block hashes must conform to a bunch of things to be used in Subxt.
|
||||
pub trait Hash:
|
||||
Debug
|
||||
+ Display
|
||||
+ Copy
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Decode
|
||||
+ AsRef<[u8]>
|
||||
+ Serialize
|
||||
+ DeserializeOwned
|
||||
+ Encode
|
||||
+ PartialEq
|
||||
+ Eq
|
||||
+ core::hash::Hash
|
||||
{
|
||||
}
|
||||
impl<T> Hash for T where
|
||||
T: Debug
|
||||
+ Display
|
||||
+ Copy
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Decode
|
||||
+ AsRef<[u8]>
|
||||
+ Serialize
|
||||
+ DeserializeOwned
|
||||
+ Encode
|
||||
+ PartialEq
|
||||
+ Eq
|
||||
+ core::hash::Hash
|
||||
{
|
||||
}
|
||||
|
||||
/// This represents the hasher used by a node to hash things like block headers
|
||||
/// and extrinsics.
|
||||
pub trait Hasher: Debug + Clone + Send + Sync + 'static {
|
||||
/// The type of hash produced by this hasher.
|
||||
type Hash: Hash;
|
||||
|
||||
/// Construct a new hasher.
|
||||
fn new(metadata: &Metadata) -> Self;
|
||||
|
||||
/// Hash some bytes to the given output type.
|
||||
fn hash(&self, s: &[u8]) -> Self::Hash;
|
||||
}
|
||||
|
||||
/// This represents the block header type used by a node.
|
||||
pub trait Header: Sized + Encode + Decode + Debug + Sync + Send + DeserializeOwned + Clone {
|
||||
/// Return the block number of this header.
|
||||
fn number(&self) -> u64;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// 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::config::transaction_extensions::CheckMortalityParams;
|
||||
|
||||
use super::{Config, HashFor};
|
||||
use super::{ExtrinsicParams, transaction_extensions};
|
||||
|
||||
/// The default [`super::ExtrinsicParams`] implementation understands common signed extensions
|
||||
/// and how to apply them to a given chain.
|
||||
pub type DefaultExtrinsicParams<T> = transaction_extensions::AnyOf<
|
||||
T,
|
||||
(
|
||||
transaction_extensions::VerifySignature<T>,
|
||||
transaction_extensions::CheckSpecVersion,
|
||||
transaction_extensions::CheckTxVersion,
|
||||
transaction_extensions::CheckNonce,
|
||||
transaction_extensions::CheckGenesis<T>,
|
||||
transaction_extensions::CheckMortality<T>,
|
||||
transaction_extensions::ChargeAssetTxPayment<T>,
|
||||
transaction_extensions::ChargeTransactionPayment,
|
||||
transaction_extensions::CheckMetadataHash,
|
||||
),
|
||||
>;
|
||||
|
||||
/// A builder that outputs the set of [`super::ExtrinsicParams::Params`] required for
|
||||
/// [`DefaultExtrinsicParams`]. This may expose methods that aren't applicable to the current
|
||||
/// chain; such values will simply be ignored if so.
|
||||
pub struct DefaultExtrinsicParamsBuilder<T: Config> {
|
||||
/// `None` means the tx will be immortal, else it's mortality is described.
|
||||
mortality: transaction_extensions::CheckMortalityParams<T>,
|
||||
/// `None` means the nonce will be automatically set.
|
||||
nonce: Option<u64>,
|
||||
/// `None` means we'll use the native token.
|
||||
tip_of_asset_id: Option<T::AssetId>,
|
||||
tip: u128,
|
||||
tip_of: u128,
|
||||
}
|
||||
|
||||
impl<T: Config> Default for DefaultExtrinsicParamsBuilder<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mortality: CheckMortalityParams::<T>::default(),
|
||||
tip: 0,
|
||||
tip_of: 0,
|
||||
tip_of_asset_id: None,
|
||||
nonce: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> DefaultExtrinsicParamsBuilder<T> {
|
||||
/// Configure new extrinsic params. We default to providing no tip
|
||||
/// and using an immortal transaction unless otherwise configured
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Make the transaction immortal, meaning it will never expire. This means that it could, in
|
||||
/// theory, be pending for a long time and only be included many blocks into the future.
|
||||
pub fn immortal(mut self) -> Self {
|
||||
self.mortality = transaction_extensions::CheckMortalityParams::<T>::immortal();
|
||||
self
|
||||
}
|
||||
|
||||
/// Make the transaction mortal, given a number of blocks it will be mortal for from
|
||||
/// the current block at the time of submission.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This will ultimately return an error if used for creating extrinsic offline, because we need
|
||||
/// additional information in order to set the mortality properly.
|
||||
///
|
||||
/// When creating offline transactions, you must use [`Self::mortal_from_unchecked`] instead to set
|
||||
/// the mortality. This provides all of the necessary information which we must otherwise be online
|
||||
/// in order to obtain.
|
||||
pub fn mortal(mut self, for_n_blocks: u64) -> Self {
|
||||
self.mortality = transaction_extensions::CheckMortalityParams::<T>::mortal(for_n_blocks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure a transaction that will be mortal for the number of blocks given, and from the
|
||||
/// block details provided. Prefer to use [`Self::mortal()`] where possible, which prevents
|
||||
/// the block number and hash from being misaligned.
|
||||
pub fn mortal_from_unchecked(
|
||||
mut self,
|
||||
for_n_blocks: u64,
|
||||
from_block_n: u64,
|
||||
from_block_hash: HashFor<T>,
|
||||
) -> Self {
|
||||
self.mortality = transaction_extensions::CheckMortalityParams::mortal_from_unchecked(
|
||||
for_n_blocks,
|
||||
from_block_n,
|
||||
from_block_hash,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a specific nonce for the submitter of the extrinsic
|
||||
pub fn nonce(mut self, nonce: u64) -> Self {
|
||||
self.nonce = Some(nonce);
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a tip to the block author in the chain's native token.
|
||||
pub fn tip(mut self, tip: u128) -> Self {
|
||||
self.tip = tip;
|
||||
self.tip_of = tip;
|
||||
self.tip_of_asset_id = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a tip to the block author using the token denominated by the `asset_id` provided. This
|
||||
/// is not applicable on chains which don't use the `ChargeAssetTxPayment` signed extension; in this
|
||||
/// case, no tip will be given.
|
||||
pub fn tip_of(mut self, tip: u128, asset_id: T::AssetId) -> Self {
|
||||
self.tip = 0;
|
||||
self.tip_of = tip;
|
||||
self.tip_of_asset_id = Some(asset_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the extrinsic parameters.
|
||||
pub fn build(self) -> <DefaultExtrinsicParams<T> as ExtrinsicParams<T>>::Params {
|
||||
let check_mortality_params = self.mortality;
|
||||
|
||||
let charge_asset_tx_params = if let Some(asset_id) = self.tip_of_asset_id {
|
||||
transaction_extensions::ChargeAssetTxPaymentParams::tip_of(self.tip, asset_id)
|
||||
} else {
|
||||
transaction_extensions::ChargeAssetTxPaymentParams::tip(self.tip)
|
||||
};
|
||||
|
||||
let charge_transaction_params =
|
||||
transaction_extensions::ChargeTransactionPaymentParams::tip(self.tip);
|
||||
|
||||
let check_nonce_params = if let Some(nonce) = self.nonce {
|
||||
transaction_extensions::CheckNonceParams::with_nonce(nonce)
|
||||
} else {
|
||||
transaction_extensions::CheckNonceParams::from_chain()
|
||||
};
|
||||
|
||||
(
|
||||
(),
|
||||
(),
|
||||
(),
|
||||
check_nonce_params,
|
||||
(),
|
||||
check_mortality_params,
|
||||
charge_asset_tx_params,
|
||||
charge_transaction_params,
|
||||
(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn assert_default<T: Default>(_t: T) {}
|
||||
|
||||
#[test]
|
||||
fn params_are_default() {
|
||||
let params = DefaultExtrinsicParamsBuilder::<crate::config::PolkadotConfig>::new().build();
|
||||
assert_default(params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// 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.
|
||||
|
||||
//! This module contains a trait which controls the parameters that must
|
||||
//! be provided in order to successfully construct an extrinsic.
|
||||
//! [`crate::config::DefaultExtrinsicParams`] provides a general-purpose
|
||||
//! implementation of this that will work in many cases.
|
||||
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::error::ExtrinsicParamsError;
|
||||
use crate::metadata::ArcMetadata;
|
||||
use core::any::Any;
|
||||
|
||||
/// This provides access to some relevant client state in transaction extensions,
|
||||
/// and is just a combination of some of the available properties.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClientState<T: Config> {
|
||||
/// Genesis hash.
|
||||
pub genesis_hash: HashFor<T>,
|
||||
/// Spec version.
|
||||
pub spec_version: u32,
|
||||
/// Transaction version.
|
||||
pub transaction_version: u32,
|
||||
/// Metadata.
|
||||
pub metadata: ArcMetadata,
|
||||
}
|
||||
|
||||
/// This trait allows you to configure the "signed extra" and
|
||||
/// "additional" parameters that are a part of the transaction payload
|
||||
/// or the signer payload respectively.
|
||||
pub trait ExtrinsicParams<T: Config>: ExtrinsicParamsEncoder + Sized + Send + 'static {
|
||||
/// These parameters can be provided to the constructor along with
|
||||
/// some default parameters that `subxt` understands, in order to
|
||||
/// help construct your [`ExtrinsicParams`] object.
|
||||
type Params: Params<T>;
|
||||
|
||||
/// Construct a new instance of our [`ExtrinsicParams`].
|
||||
fn new(client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError>;
|
||||
}
|
||||
|
||||
/// This trait is expected to be implemented for any [`ExtrinsicParams`], and
|
||||
/// defines how to encode the "additional" and "extra" params. Both functions
|
||||
/// are optional and will encode nothing by default.
|
||||
pub trait ExtrinsicParamsEncoder: 'static {
|
||||
/// This is expected to SCALE encode the transaction extension data to some
|
||||
/// buffer that has been provided. This data is attached to the transaction
|
||||
/// and also (by default) attached to the signer payload which is signed to
|
||||
/// provide a signature for the transaction.
|
||||
///
|
||||
/// If [`ExtrinsicParamsEncoder::encode_signer_payload_value_to`] is implemented,
|
||||
/// then that will be used instead when generating a signer payload. Useful for
|
||||
/// eg the `VerifySignature` extension, which is send with the transaction but
|
||||
/// is not a part of the signer payload.
|
||||
fn encode_value_to(&self, _v: &mut Vec<u8>) {}
|
||||
|
||||
/// See [`ExtrinsicParamsEncoder::encode_value_to`]. This defaults to calling that
|
||||
/// method, but if implemented will dictate what is encoded to the signer payload.
|
||||
fn encode_signer_payload_value_to(&self, v: &mut Vec<u8>) {
|
||||
self.encode_value_to(v);
|
||||
}
|
||||
|
||||
/// This is expected to SCALE encode the "implicit" (formally "additional")
|
||||
/// parameters to some buffer that has been provided. These parameters are
|
||||
/// _not_ sent along with the transaction, but are taken into account when
|
||||
/// signing it, meaning the client and node must agree on their values.
|
||||
fn encode_implicit_to(&self, _v: &mut Vec<u8>) {}
|
||||
|
||||
/// Set the signature. This happens after we have constructed the extrinsic params,
|
||||
/// and so is defined here rather than on the params, below. We need to use `&dyn Any`
|
||||
/// to keep this trait object safe, but can downcast in the impls.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Implementations of this will likely try to downcast the provided `account_id`
|
||||
/// and `signature` into `T::AccountId` and `T::Signature` (where `T: Config`), and are
|
||||
/// free to panic if this downcasting does not succeed.
|
||||
///
|
||||
/// In typical usage, this is not a problem, since this method is only called internally
|
||||
/// and provided values which line up with the relevant `Config`. In theory though, this
|
||||
/// method can be called manually with any types, hence this warning.
|
||||
fn inject_signature(&mut self, _account_id: &dyn Any, _signature: &dyn Any) {}
|
||||
}
|
||||
|
||||
/// The parameters (ie [`ExtrinsicParams::Params`]) can also have data injected into them,
|
||||
/// allowing Subxt to retrieve data from the chain and amend the parameters with it when
|
||||
/// online.
|
||||
pub trait Params<T: Config> {
|
||||
/// Set the account nonce.
|
||||
fn inject_account_nonce(&mut self, _nonce: u64) {}
|
||||
/// Set the current block.
|
||||
fn inject_block(&mut self, _number: u64, _hash: HashFor<T>) {}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for () {}
|
||||
|
||||
macro_rules! impl_tuples {
|
||||
($($ident:ident $index:tt),+) => {
|
||||
impl <Conf: Config, $($ident : Params<Conf>),+> Params<Conf> for ($($ident,)+){
|
||||
fn inject_account_nonce(&mut self, nonce: u64) {
|
||||
$(self.$index.inject_account_nonce(nonce);)+
|
||||
}
|
||||
|
||||
fn inject_block(&mut self, number: u64, hash: HashFor<Conf>) {
|
||||
$(self.$index.inject_block(number, hash);)+
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const _: () = {
|
||||
impl_tuples!(A 0);
|
||||
impl_tuples!(A 0, B 1);
|
||||
impl_tuples!(A 0, B 1, C 2);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22, X 23);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22, X 23, Y 24);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22, X 23, Y 24, Z 25);
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
// 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.
|
||||
|
||||
//! Polkadot specific configuration
|
||||
|
||||
use super::{Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder};
|
||||
|
||||
use crate::config::substrate::{SubstrateConfig, SubstrateConfigBuilder};
|
||||
use crate::metadata::ArcMetadata;
|
||||
use scale_info_legacy::TypeRegistrySet;
|
||||
|
||||
pub use crate::config::substrate::{SpecVersionForRange, SubstrateHeader};
|
||||
pub use crate::utils::{AccountId32, MultiAddress, MultiSignature};
|
||||
pub use primitive_types::{H256, U256};
|
||||
|
||||
/// Construct a [`PolkadotConfig`] using this.
|
||||
pub struct PolkadotConfigBuilder(SubstrateConfigBuilder);
|
||||
|
||||
impl Default for PolkadotConfigBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PolkadotConfigBuilder {
|
||||
/// Create a new [`PolkadotConfigBuilder`].
|
||||
pub fn new() -> Self {
|
||||
let inner = SubstrateConfigBuilder::new()
|
||||
.set_legacy_types(frame_decode::legacy_types::polkadot::relay_chain());
|
||||
|
||||
PolkadotConfigBuilder(inner)
|
||||
}
|
||||
|
||||
/// Set the metadata to be used for decoding blocks at the given spec versions.
|
||||
pub fn set_metadata_for_spec_versions(
|
||||
mut self,
|
||||
ranges: impl Iterator<Item = (u32, ArcMetadata)>,
|
||||
) -> Self {
|
||||
self = Self(self.0.set_metadata_for_spec_versions(ranges));
|
||||
self
|
||||
}
|
||||
|
||||
/// Given an iterator of block ranges to spec version of the form `(start, end, spec_version)`, add them
|
||||
/// to this configuration.
|
||||
pub fn set_spec_version_for_block_ranges(
|
||||
mut self,
|
||||
ranges: impl Iterator<Item = SpecVersionForRange>,
|
||||
) -> Self {
|
||||
self = Self(self.0.set_spec_version_for_block_ranges(ranges));
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct the [`PolkadotConfig`] from this builder.
|
||||
pub fn build(self) -> PolkadotConfig {
|
||||
PolkadotConfig(self.0.build())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration that's suitable for the Polkadot Relay Chain.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PolkadotConfig(SubstrateConfig);
|
||||
|
||||
impl PolkadotConfig {
|
||||
/// Create a new, default, [`PolkadotConfig`].
|
||||
pub fn new() -> Self {
|
||||
Self::builder().build()
|
||||
}
|
||||
|
||||
/// Build a new [`PolkadotConfig`].
|
||||
pub fn builder() -> PolkadotConfigBuilder {
|
||||
PolkadotConfigBuilder(SubstrateConfig::builder())
|
||||
}
|
||||
}
|
||||
|
||||
impl Config for PolkadotConfig {
|
||||
type AccountId = <SubstrateConfig as Config>::AccountId;
|
||||
type Signature = <SubstrateConfig as Config>::Signature;
|
||||
type Hasher = <SubstrateConfig as Config>::Hasher;
|
||||
type Header = <SubstrateConfig as Config>::Header;
|
||||
type AssetId = <SubstrateConfig as Config>::AssetId;
|
||||
|
||||
// Address on Polkadot has no account index, whereas it's u32 on
|
||||
// the default substrate dev node.
|
||||
type Address = MultiAddress<Self::AccountId, ()>;
|
||||
|
||||
// These are the same as the default substrate node, but redefined
|
||||
// because we need to pass the PolkadotConfig trait as a param.
|
||||
type ExtrinsicParams = PolkadotExtrinsicParams<Self>;
|
||||
|
||||
fn genesis_hash(&self) -> Option<super::HashFor<Self>> {
|
||||
self.0.genesis_hash()
|
||||
}
|
||||
|
||||
fn legacy_types_for_spec_version(&'_ self, spec_version: u32) -> Option<TypeRegistrySet<'_>> {
|
||||
self.0.legacy_types_for_spec_version(spec_version)
|
||||
}
|
||||
|
||||
fn spec_and_transaction_version_for_block_number(
|
||||
&self,
|
||||
block_number: u64,
|
||||
) -> Option<(u32, u32)> {
|
||||
self.0
|
||||
.spec_and_transaction_version_for_block_number(block_number)
|
||||
}
|
||||
|
||||
fn metadata_for_spec_version(&self, spec_version: u32) -> Option<ArcMetadata> {
|
||||
self.0.metadata_for_spec_version(spec_version)
|
||||
}
|
||||
|
||||
fn set_metadata_for_spec_version(&self, spec_version: u32, metadata: ArcMetadata) {
|
||||
self.0.set_metadata_for_spec_version(spec_version, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct representing the signed extra and additional parameters required
|
||||
/// to construct a transaction for a polkadot node.
|
||||
pub type PolkadotExtrinsicParams<T> = DefaultExtrinsicParams<T>;
|
||||
|
||||
/// A builder which leads to [`PolkadotExtrinsicParams`] being constructed.
|
||||
/// This is what you provide to methods like `sign_and_submit()`.
|
||||
pub type PolkadotExtrinsicParamsBuilder<T> = DefaultExtrinsicParamsBuilder<T>;
|
||||
@@ -0,0 +1,555 @@
|
||||
// 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.
|
||||
|
||||
//! Substrate specific configuration
|
||||
|
||||
use super::{Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, Hasher, Header};
|
||||
use crate::config::Hash;
|
||||
use crate::metadata::{ArcMetadata, Metadata};
|
||||
use crate::utils::RangeMap;
|
||||
pub use crate::utils::{AccountId32, MultiAddress, MultiSignature};
|
||||
use codec::{Decode, Encode};
|
||||
pub use primitive_types::{H256, U256};
|
||||
use scale_info_legacy::{ChainTypeRegistry, TypeRegistrySet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Construct a [`SubstrateConfig`] using this.
|
||||
pub struct SubstrateConfigBuilder {
|
||||
legacy_types: Option<ChainTypeRegistry>,
|
||||
spec_and_transaction_version_for_block_number: RangeMap<u64, (u32, u32)>,
|
||||
genesis_hash: Option<H256>,
|
||||
metadata_for_spec_version: Mutex<HashMap<u32, ArcMetadata>>,
|
||||
use_old_v9_hashers_before_spec_version: u32,
|
||||
}
|
||||
|
||||
impl Default for SubstrateConfigBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SubstrateConfigBuilder {
|
||||
/// Create a new builder to construct a [`SubstrateConfig`] from.
|
||||
pub fn new() -> Self {
|
||||
SubstrateConfigBuilder {
|
||||
legacy_types: None,
|
||||
genesis_hash: None,
|
||||
spec_and_transaction_version_for_block_number: RangeMap::empty(),
|
||||
metadata_for_spec_version: Mutex::new(HashMap::new()),
|
||||
use_old_v9_hashers_before_spec_version: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the genesis hash for this chain.
|
||||
pub fn set_genesis_hash(mut self, genesis_hash: H256) -> Self {
|
||||
self.genesis_hash = Some(genesis_hash);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the legacy types to use for this configuration. This enables support for
|
||||
/// blocks produced by Runtimes that emit metadata older than V14.
|
||||
pub fn set_legacy_types(mut self, legacy_types: ChainTypeRegistry) -> Self {
|
||||
self.legacy_types = Some(legacy_types);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the metadata to be used for decoding blocks at the given spec versions.
|
||||
pub fn set_metadata_for_spec_versions(
|
||||
self,
|
||||
ranges: impl Iterator<Item = (u32, ArcMetadata)>,
|
||||
) -> Self {
|
||||
let mut map = self.metadata_for_spec_version.lock().unwrap();
|
||||
for (spec_version, metadata) in ranges {
|
||||
map.insert(spec_version, metadata);
|
||||
}
|
||||
drop(map);
|
||||
self
|
||||
}
|
||||
|
||||
/// Given an iterator of block ranges to spec version of the form `(start, end, spec_version)`, add them
|
||||
/// to this configuration.
|
||||
pub fn set_spec_version_for_block_ranges(
|
||||
mut self,
|
||||
ranges: impl Iterator<Item = SpecVersionForRange>,
|
||||
) -> Self {
|
||||
let mut m = RangeMap::builder();
|
||||
for version_for_range in ranges {
|
||||
let start = version_for_range.block_range.start;
|
||||
let end = version_for_range.block_range.end;
|
||||
let spec_version = version_for_range.spec_version;
|
||||
let transaction_version = version_for_range.transaction_version;
|
||||
m = m.add_range(start, end, (spec_version, transaction_version));
|
||||
}
|
||||
self.spec_and_transaction_version_for_block_number = m.build();
|
||||
self
|
||||
}
|
||||
|
||||
/// The storage hasher encoding/decoding changed during V9 metadata. By default we support the "new" version
|
||||
/// of things. We can use this option to support the old version of things prior to a given spec version.
|
||||
pub fn use_old_v9_hashers_before_spec_version(mut self, spec_version: u32) -> Self {
|
||||
self.use_old_v9_hashers_before_spec_version = spec_version;
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct the [`SubstrateConfig`] from this builder.
|
||||
pub fn build(self) -> SubstrateConfig {
|
||||
SubstrateConfig {
|
||||
inner: Arc::new(SubstrateConfigInner {
|
||||
legacy_types: self.legacy_types,
|
||||
spec_and_transaction_version_for_block_number: self
|
||||
.spec_and_transaction_version_for_block_number,
|
||||
metadata_for_spec_version: self.metadata_for_spec_version,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Define a spec version for a range of blocks. The new spec version is expected
|
||||
/// to begin at the first block in the range and end just prior to the last block
|
||||
/// in the range.
|
||||
pub struct SpecVersionForRange {
|
||||
/// The block range that this spec version applies to. Inclusive of the start
|
||||
/// and exclusive of the enc.
|
||||
pub block_range: std::ops::Range<u64>,
|
||||
/// The spec version at this block range.
|
||||
pub spec_version: u32,
|
||||
/// The transaction version at this block range.
|
||||
pub transaction_version: u32,
|
||||
}
|
||||
|
||||
/// Configuration that's suitable for standard Substrate chains (ie those
|
||||
/// that have not customized the block hash type).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubstrateConfig {
|
||||
inner: Arc<SubstrateConfigInner>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SubstrateConfigInner {
|
||||
legacy_types: Option<ChainTypeRegistry>,
|
||||
spec_and_transaction_version_for_block_number: RangeMap<u64, (u32, u32)>,
|
||||
metadata_for_spec_version: Mutex<HashMap<u32, ArcMetadata>>,
|
||||
}
|
||||
|
||||
impl SubstrateConfig {
|
||||
/// Create a new, default, [`SubstrateConfig`]. This does not
|
||||
/// support working with historic (pre-V14) types. If you want this,
|
||||
/// then use [`SubstrateConfig::builder()`] and then provide legacy
|
||||
/// types via [`SubstrateConfigBuilder::set_legacy_types()`].
|
||||
pub fn new() -> Self {
|
||||
Self::builder().build()
|
||||
}
|
||||
|
||||
/// Build a new [`SubstrateConfig`].
|
||||
pub fn builder() -> SubstrateConfigBuilder {
|
||||
SubstrateConfigBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Config for SubstrateConfig {
|
||||
type AccountId = AccountId32;
|
||||
type Address = MultiAddress<Self::AccountId, u32>;
|
||||
type Signature = MultiSignature;
|
||||
type Hasher = DynamicHasher256;
|
||||
type Header = SubstrateHeader<<Self::Hasher as Hasher>::Hash>;
|
||||
type ExtrinsicParams = SubstrateExtrinsicParams<Self>;
|
||||
type AssetId = u32;
|
||||
|
||||
fn legacy_types_for_spec_version(&'_ self, spec_version: u32) -> Option<TypeRegistrySet<'_>> {
|
||||
self.inner
|
||||
.legacy_types
|
||||
.as_ref()
|
||||
.map(|types| types.for_spec_version(spec_version as u64))
|
||||
}
|
||||
|
||||
fn spec_and_transaction_version_for_block_number(
|
||||
&self,
|
||||
block_number: u64,
|
||||
) -> Option<(u32, u32)> {
|
||||
self.inner
|
||||
.spec_and_transaction_version_for_block_number
|
||||
.get(block_number)
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn metadata_for_spec_version(&self, spec_version: u32) -> Option<ArcMetadata> {
|
||||
self.inner
|
||||
.metadata_for_spec_version
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&spec_version)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn set_metadata_for_spec_version(&self, spec_version: u32, metadata: ArcMetadata) {
|
||||
self.inner
|
||||
.metadata_for_spec_version
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(spec_version, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct representing the signed extra and additional parameters required
|
||||
/// to construct a transaction for the default substrate node.
|
||||
pub type SubstrateExtrinsicParams<T> = DefaultExtrinsicParams<T>;
|
||||
|
||||
/// A builder which leads to [`SubstrateExtrinsicParams`] being constructed.
|
||||
/// This is what you provide to methods like `sign_and_submit()`.
|
||||
pub type SubstrateExtrinsicParamsBuilder<T> = DefaultExtrinsicParamsBuilder<T>;
|
||||
|
||||
/// A hasher (ie implements [`Hasher`]) which hashes values using the blaks2_256 algorithm.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BlakeTwo256;
|
||||
|
||||
impl Hasher for BlakeTwo256 {
|
||||
type Hash = H256;
|
||||
|
||||
fn new(_metadata: &Metadata) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn hash(&self, s: &[u8]) -> Self::Hash {
|
||||
sp_crypto_hashing::blake2_256(s).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A hasher (ie implements [`Hasher`]) which inspects the runtime metadata to decide how to
|
||||
/// hash types, falling back to blake2_256 if the hasher information is not available.
|
||||
///
|
||||
/// Currently this hasher supports only `BlakeTwo256` and `Keccak256` hashing methods.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DynamicHasher256(HashType);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum HashType {
|
||||
// Most chains use this:
|
||||
BlakeTwo256,
|
||||
// Chains like Hyperbridge use this (tends to be eth compatible chains)
|
||||
Keccak256,
|
||||
// If we don't have V16 metadata, we'll emit this and default to BlakeTwo256.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Hasher for DynamicHasher256 {
|
||||
type Hash = H256;
|
||||
|
||||
fn new(metadata: &Metadata) -> Self {
|
||||
// Determine the Hash associated type used for the current chain, if possible.
|
||||
let Some(system_pallet) = metadata.pallet_by_name("System") else {
|
||||
return Self(HashType::Unknown);
|
||||
};
|
||||
let Some(hash_ty_id) = system_pallet.associated_type_id("Hashing") else {
|
||||
return Self(HashType::Unknown);
|
||||
};
|
||||
|
||||
let ty = metadata
|
||||
.types()
|
||||
.resolve(hash_ty_id)
|
||||
.expect("Type information for 'Hashing' associated type should be in metadata");
|
||||
|
||||
let hash_type = match ty.path.ident().as_deref().unwrap_or("") {
|
||||
"BlakeTwo256" => HashType::BlakeTwo256,
|
||||
"Keccak256" => HashType::Keccak256,
|
||||
_ => HashType::Unknown,
|
||||
};
|
||||
|
||||
Self(hash_type)
|
||||
}
|
||||
|
||||
fn hash(&self, s: &[u8]) -> Self::Hash {
|
||||
match self.0 {
|
||||
HashType::BlakeTwo256 | HashType::Unknown => sp_crypto_hashing::blake2_256(s).into(),
|
||||
HashType::Keccak256 => sp_crypto_hashing::keccak_256(s).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic Substrate header type, adapted from `sp_runtime::generic::Header`.
|
||||
/// The block number and hasher can be configured to adapt this for other nodes.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubstrateHeader<Hash> {
|
||||
/// The parent hash.
|
||||
pub parent_hash: Hash,
|
||||
/// The block number.
|
||||
#[serde(
|
||||
serialize_with = "serialize_number",
|
||||
deserialize_with = "deserialize_number"
|
||||
)]
|
||||
#[codec(compact)]
|
||||
pub number: u64,
|
||||
/// The state trie merkle root
|
||||
pub state_root: Hash,
|
||||
/// The merkle root of the extrinsics.
|
||||
pub extrinsics_root: Hash,
|
||||
/// A chain-specific digest of data useful for light clients or referencing auxiliary data.
|
||||
pub digest: Digest,
|
||||
}
|
||||
|
||||
impl<H> Header for SubstrateHeader<H>
|
||||
where
|
||||
H: Hash,
|
||||
SubstrateHeader<H>: Encode + Decode,
|
||||
{
|
||||
fn number(&self) -> u64 {
|
||||
self.number.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic header digest. From `sp_runtime::generic::digest`.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Digest {
|
||||
/// A list of digest items.
|
||||
pub logs: Vec<DigestItem>,
|
||||
}
|
||||
|
||||
/// Digest item that is able to encode/decode 'system' digest items and
|
||||
/// provide opaque access to other items. From `sp_runtime::generic::digest`.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum DigestItem {
|
||||
/// A pre-runtime digest.
|
||||
///
|
||||
/// These are messages from the consensus engine to the runtime, although
|
||||
/// the consensus engine can (and should) read them itself to avoid
|
||||
/// code and state duplication. It is erroneous for a runtime to produce
|
||||
/// these, but this is not (yet) checked.
|
||||
///
|
||||
/// NOTE: the runtime is not allowed to panic or fail in an `on_initialize`
|
||||
/// call if an expected `PreRuntime` digest is not present. It is the
|
||||
/// responsibility of a external block verifier to check this. Runtime API calls
|
||||
/// will initialize the block without pre-runtime digests, so initialization
|
||||
/// cannot fail when they are missing.
|
||||
PreRuntime(ConsensusEngineId, Vec<u8>),
|
||||
|
||||
/// A message from the runtime to the consensus engine. This should *never*
|
||||
/// be generated by the native code of any consensus engine, but this is not
|
||||
/// checked (yet).
|
||||
Consensus(ConsensusEngineId, Vec<u8>),
|
||||
|
||||
/// Put a Seal on it. This is only used by native code, and is never seen
|
||||
/// by runtimes.
|
||||
Seal(ConsensusEngineId, Vec<u8>),
|
||||
|
||||
/// Some other thing. Unsupported and experimental.
|
||||
Other(Vec<u8>),
|
||||
|
||||
/// An indication for the light clients that the runtime execution
|
||||
/// environment is updated.
|
||||
///
|
||||
/// Currently this is triggered when:
|
||||
/// 1. Runtime code blob is changed or
|
||||
/// 2. `heap_pages` value is changed.
|
||||
RuntimeEnvironmentUpdated,
|
||||
}
|
||||
|
||||
// From sp_runtime::generic, DigestItem enum indexes are encoded using this:
|
||||
#[repr(u32)]
|
||||
#[derive(Encode, Decode)]
|
||||
enum DigestItemType {
|
||||
Other = 0u32,
|
||||
Consensus = 4u32,
|
||||
Seal = 5u32,
|
||||
PreRuntime = 6u32,
|
||||
RuntimeEnvironmentUpdated = 8u32,
|
||||
}
|
||||
impl Encode for DigestItem {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut v = Vec::new();
|
||||
|
||||
match self {
|
||||
Self::Consensus(val, data) => {
|
||||
DigestItemType::Consensus.encode_to(&mut v);
|
||||
(val, data).encode_to(&mut v);
|
||||
}
|
||||
Self::Seal(val, sig) => {
|
||||
DigestItemType::Seal.encode_to(&mut v);
|
||||
(val, sig).encode_to(&mut v);
|
||||
}
|
||||
Self::PreRuntime(val, data) => {
|
||||
DigestItemType::PreRuntime.encode_to(&mut v);
|
||||
(val, data).encode_to(&mut v);
|
||||
}
|
||||
Self::Other(val) => {
|
||||
DigestItemType::Other.encode_to(&mut v);
|
||||
val.encode_to(&mut v);
|
||||
}
|
||||
Self::RuntimeEnvironmentUpdated => {
|
||||
DigestItemType::RuntimeEnvironmentUpdated.encode_to(&mut v);
|
||||
}
|
||||
}
|
||||
|
||||
v
|
||||
}
|
||||
}
|
||||
impl Decode for DigestItem {
|
||||
fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
|
||||
let item_type: DigestItemType = Decode::decode(input)?;
|
||||
match item_type {
|
||||
DigestItemType::PreRuntime => {
|
||||
let vals: (ConsensusEngineId, Vec<u8>) = Decode::decode(input)?;
|
||||
Ok(Self::PreRuntime(vals.0, vals.1))
|
||||
}
|
||||
DigestItemType::Consensus => {
|
||||
let vals: (ConsensusEngineId, Vec<u8>) = Decode::decode(input)?;
|
||||
Ok(Self::Consensus(vals.0, vals.1))
|
||||
}
|
||||
DigestItemType::Seal => {
|
||||
let vals: (ConsensusEngineId, Vec<u8>) = Decode::decode(input)?;
|
||||
Ok(Self::Seal(vals.0, vals.1))
|
||||
}
|
||||
DigestItemType::Other => Ok(Self::Other(Decode::decode(input)?)),
|
||||
DigestItemType::RuntimeEnvironmentUpdated => Ok(Self::RuntimeEnvironmentUpdated),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Consensus engine unique ID. From `sp_runtime::ConsensusEngineId`.
|
||||
pub type ConsensusEngineId = [u8; 4];
|
||||
|
||||
impl serde::Serialize for DigestItem {
|
||||
fn serialize<S>(&self, seq: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.using_encoded(|bytes| impl_serde::serialize::serialize(bytes, seq))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> serde::Deserialize<'a> for DigestItem {
|
||||
fn deserialize<D>(de: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'a>,
|
||||
{
|
||||
let r = impl_serde::serialize::deserialize(de)?;
|
||||
Decode::decode(&mut &r[..])
|
||||
.map_err(|e| serde::de::Error::custom(format!("Decode error: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_number<S, T: Copy + Into<U256>>(val: &T, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let u256: U256 = (*val).into();
|
||||
serde::Serialize::serialize(&u256, s)
|
||||
}
|
||||
|
||||
fn deserialize_number<'a, D, T: TryFrom<U256>>(d: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'a>,
|
||||
{
|
||||
// At the time of writing, Smoldot gives back block numbers in numeric rather
|
||||
// than hex format. So let's support deserializing from both here:
|
||||
let number_or_hex = NumberOrHex::deserialize(d)?;
|
||||
let u256 = number_or_hex.into_u256();
|
||||
TryFrom::try_from(u256).map_err(|_| serde::de::Error::custom("Try from failed"))
|
||||
}
|
||||
|
||||
/// A number type that can be serialized both as a number or a string that encodes a number in a
|
||||
/// string.
|
||||
///
|
||||
/// We allow two representations of the block number as input. Either we deserialize to the type
|
||||
/// that is specified in the block type or we attempt to parse given hex value.
|
||||
///
|
||||
/// The primary motivation for having this type is to avoid overflows when using big integers in
|
||||
/// JavaScript (which we consider as an important RPC API consumer).
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum NumberOrHex {
|
||||
/// The number represented directly.
|
||||
Number(u64),
|
||||
/// Hex representation of the number.
|
||||
Hex(U256),
|
||||
}
|
||||
|
||||
impl NumberOrHex {
|
||||
/// Converts this number into an U256.
|
||||
pub fn into_u256(self) -> U256 {
|
||||
match self {
|
||||
NumberOrHex::Number(n) => n.into(),
|
||||
NumberOrHex::Hex(h) => h,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NumberOrHex> for U256 {
|
||||
fn from(num_or_hex: NumberOrHex) -> U256 {
|
||||
num_or_hex.into_u256()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! into_number_or_hex {
|
||||
($($t: ty)+) => {
|
||||
$(
|
||||
impl From<$t> for NumberOrHex {
|
||||
fn from(x: $t) -> Self {
|
||||
NumberOrHex::Number(x.into())
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
into_number_or_hex!(u8 u16 u32 u64);
|
||||
|
||||
impl From<u128> for NumberOrHex {
|
||||
fn from(n: u128) -> Self {
|
||||
NumberOrHex::Hex(n.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<U256> for NumberOrHex {
|
||||
fn from(n: U256) -> Self {
|
||||
NumberOrHex::Hex(n)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
// Smoldot returns numeric block numbers in the header at the time of writing;
|
||||
// ensure we can deserialize them properly.
|
||||
#[test]
|
||||
fn can_deserialize_numeric_block_number() {
|
||||
let numeric_block_number_json = r#"
|
||||
{
|
||||
"digest": {
|
||||
"logs": []
|
||||
},
|
||||
"extrinsicsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"number": 4,
|
||||
"parentHash": "0xcb2690b2c85ceab55be03fc7f7f5f3857e7efeb7a020600ebd4331e10be2f7a5",
|
||||
"stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
"#;
|
||||
|
||||
let header: SubstrateHeader<H256> =
|
||||
serde_json::from_str(numeric_block_number_json).expect("valid block header");
|
||||
assert_eq!(header.number(), 4);
|
||||
}
|
||||
|
||||
// Substrate returns hex block numbers; ensure we can also deserialize those OK.
|
||||
#[test]
|
||||
fn can_deserialize_hex_block_number() {
|
||||
let numeric_block_number_json = r#"
|
||||
{
|
||||
"digest": {
|
||||
"logs": []
|
||||
},
|
||||
"extrinsicsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"number": "0x04",
|
||||
"parentHash": "0xcb2690b2c85ceab55be03fc7f7f5f3857e7efeb7a020600ebd4331e10be2f7a5",
|
||||
"stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
"#;
|
||||
|
||||
let header: SubstrateHeader<H256> =
|
||||
serde_json::from_str(numeric_block_number_json).expect("valid block header");
|
||||
assert_eq!(header.number(), 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,703 @@
|
||||
// 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.
|
||||
|
||||
//! This module contains implementations for common transaction extensions, each
|
||||
//! of which implements [`TransactionExtension`], and can be used in conjunction with
|
||||
//! [`AnyOf`] to configure the set of transaction extensions which are known about
|
||||
//! when interacting with a chain.
|
||||
|
||||
use super::extrinsic_params::{ClientState, ExtrinsicParams};
|
||||
use crate::config::ExtrinsicParamsEncoder;
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::error::ExtrinsicParamsError;
|
||||
use crate::utils::{Era, Static};
|
||||
use codec::{Compact, Encode};
|
||||
use core::any::Any;
|
||||
use core::fmt::Debug;
|
||||
use derive_where::derive_where;
|
||||
use scale_decode::DecodeAsType;
|
||||
use scale_info::PortableRegistry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Re-export this here; it's a bit generically named to be re-exported from ::config.
|
||||
pub use super::extrinsic_params::Params;
|
||||
|
||||
/// A single [`TransactionExtension`] has a unique name, but is otherwise the
|
||||
/// same as [`ExtrinsicParams`] in describing how to encode the extra and
|
||||
/// additional data.
|
||||
pub trait TransactionExtension<T: Config>: ExtrinsicParams<T> {
|
||||
/// The type representing the `extra` / value bytes of a transaction extension.
|
||||
/// Decoding from this type should be symmetrical to the respective
|
||||
/// `ExtrinsicParamsEncoder::encode_value_to()` implementation of this transaction extension.
|
||||
type Decoded: DecodeAsType;
|
||||
|
||||
/// This should return true if the transaction extension matches the details given.
|
||||
/// Often, this will involve just checking that the identifier given matches that of the
|
||||
/// extension in question.
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool;
|
||||
}
|
||||
|
||||
/// The [`VerifySignature`] extension. For V5 General transactions, this is how a signature
|
||||
/// is provided. The signature is constructed by signing a payload which contains the
|
||||
/// transaction call data as well as the encoded "additional" bytes for any extensions _after_
|
||||
/// this one in the list.
|
||||
pub struct VerifySignature<T: Config>(VerifySignatureDetails<T>);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for VerifySignature<T> {
|
||||
type Params = ();
|
||||
|
||||
fn new(_client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(VerifySignature(VerifySignatureDetails::Disabled))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParamsEncoder for VerifySignature<T> {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
self.0.encode_to(v);
|
||||
}
|
||||
fn encode_signer_payload_value_to(&self, v: &mut Vec<u8>) {
|
||||
// This extension is never encoded to the signer payload, and extensions
|
||||
// prior to this are ignored when creating said payload, so clear anything
|
||||
// we've seen so far.
|
||||
v.clear();
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
// We only use the "implicit" data for extensions _after_ this one
|
||||
// in the pipeline to form the signer payload. Thus, clear anything
|
||||
// we've seen so far.
|
||||
v.clear();
|
||||
}
|
||||
|
||||
fn inject_signature(&mut self, account: &dyn Any, signature: &dyn Any) {
|
||||
// Downcast refs back to concrete types (we use `&dyn Any`` so that the trait remains object safe)
|
||||
let account = account
|
||||
.downcast_ref::<T::AccountId>()
|
||||
.expect("A T::AccountId should have been provided")
|
||||
.clone();
|
||||
let signature = signature
|
||||
.downcast_ref::<T::Signature>()
|
||||
.expect("A T::Signature should have been provided")
|
||||
.clone();
|
||||
|
||||
// The signature is not set through params, only here, once given by a user:
|
||||
self.0 = VerifySignatureDetails::Signed { signature, account }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for VerifySignature<T> {
|
||||
type Decoded = Static<VerifySignatureDetails<T>>;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "VerifySignature"
|
||||
}
|
||||
}
|
||||
|
||||
/// This allows a signature to be provided to the [`VerifySignature`] transaction extension.
|
||||
// Dev note: this must encode identically to https://github.com/paritytech/polkadot-sdk/blob/fd72d58313c297a10600037ce1bb88ec958d722e/substrate/frame/verify-signature/src/extension.rs#L43
|
||||
#[derive(codec::Encode, codec::Decode)]
|
||||
pub enum VerifySignatureDetails<T: Config> {
|
||||
/// A signature has been provided.
|
||||
Signed {
|
||||
/// The signature.
|
||||
signature: T::Signature,
|
||||
/// The account that generated the signature.
|
||||
account: T::AccountId,
|
||||
},
|
||||
/// No signature was provided.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// The [`CheckMetadataHash`] transaction extension.
|
||||
pub struct CheckMetadataHash {
|
||||
// Eventually we might provide or calculate the metadata hash here,
|
||||
// but for now we never provide a hash and so this is empty.
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckMetadataHash {
|
||||
type Params = ();
|
||||
|
||||
fn new(_client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckMetadataHash {})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for CheckMetadataHash {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
// A single 0 byte in the TX payload indicates that the chain should
|
||||
// _not_ expect any metadata hash to exist in the signer payload.
|
||||
0u8.encode_to(v);
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
// We provide no metadata hash in the signer payload to align with the above.
|
||||
None::<()>.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckMetadataHash {
|
||||
type Decoded = CheckMetadataHashMode;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckMetadataHash"
|
||||
}
|
||||
}
|
||||
|
||||
/// Is metadata checking enabled or disabled?
|
||||
// Dev note: The "Disabled" and "Enabled" variant names match those that the
|
||||
// transaction extension will be encoded with, in order that DecodeAsType will work
|
||||
// properly.
|
||||
#[derive(Copy, Clone, Debug, DecodeAsType)]
|
||||
pub enum CheckMetadataHashMode {
|
||||
/// No hash was provided in the signer payload.
|
||||
Disabled,
|
||||
/// A hash was provided in the signer payload.
|
||||
Enabled,
|
||||
}
|
||||
|
||||
impl CheckMetadataHashMode {
|
||||
/// Is metadata checking enabled or disabled for this transaction?
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
match self {
|
||||
CheckMetadataHashMode::Disabled => false,
|
||||
CheckMetadataHashMode::Enabled => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckSpecVersion`] transaction extension.
|
||||
pub struct CheckSpecVersion(u32);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckSpecVersion {
|
||||
type Params = ();
|
||||
|
||||
fn new(client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckSpecVersion(client.spec_version))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for CheckSpecVersion {
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
self.0.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckSpecVersion {
|
||||
type Decoded = ();
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckSpecVersion"
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckNonce`] transaction extension.
|
||||
pub struct CheckNonce(u64);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckNonce {
|
||||
type Params = CheckNonceParams;
|
||||
|
||||
fn new(_client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckNonce(params.0.unwrap_or(0)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for CheckNonce {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
Compact(self.0).encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckNonce {
|
||||
type Decoded = u64;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckNonce"
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure the nonce used.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CheckNonceParams(Option<u64>);
|
||||
|
||||
impl CheckNonceParams {
|
||||
/// Retrieve the nonce from the chain and use that.
|
||||
pub fn from_chain() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
/// Manually set an account nonce to use.
|
||||
pub fn with_nonce(nonce: u64) -> Self {
|
||||
Self(Some(nonce))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for CheckNonceParams {
|
||||
fn inject_account_nonce(&mut self, nonce: u64) {
|
||||
if self.0.is_none() {
|
||||
self.0 = Some(nonce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckTxVersion`] transaction extension.
|
||||
pub struct CheckTxVersion(u32);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckTxVersion {
|
||||
type Params = ();
|
||||
|
||||
fn new(client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckTxVersion(client.transaction_version))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for CheckTxVersion {
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
self.0.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckTxVersion {
|
||||
type Decoded = ();
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckTxVersion"
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckGenesis`] transaction extension.
|
||||
pub struct CheckGenesis<T: Config>(HashFor<T>);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckGenesis<T> {
|
||||
type Params = ();
|
||||
|
||||
fn new(client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckGenesis(client.genesis_hash))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParamsEncoder for CheckGenesis<T> {
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
self.0.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckGenesis<T> {
|
||||
type Decoded = ();
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckGenesis"
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckMortality`] transaction extension.
|
||||
pub struct CheckMortality<T: Config> {
|
||||
params: CheckMortalityParamsInner<T>,
|
||||
genesis_hash: HashFor<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckMortality<T> {
|
||||
type Params = CheckMortalityParams<T>;
|
||||
|
||||
fn new(client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
// If a user has explicitly configured the transaction to be mortal for n blocks, but we get
|
||||
// to this stage and no injected information was able to turn this into MortalFromBlock{..},
|
||||
// then we hit an error as we are unable to construct a mortal transaction here.
|
||||
if matches!(¶ms.0, CheckMortalityParamsInner::MortalForBlocks(_)) {
|
||||
return Err(ExtrinsicParamsError::custom(
|
||||
"CheckMortality: We cannot construct an offline extrinsic with only the number of blocks it is mortal for. Use mortal_from_unchecked instead.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(CheckMortality {
|
||||
// if nothing has been explicitly configured, we will have a mortal transaction
|
||||
// valid for 32 blocks if block info is available.
|
||||
params: params.0,
|
||||
genesis_hash: client.genesis_hash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParamsEncoder for CheckMortality<T> {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
match &self.params {
|
||||
CheckMortalityParamsInner::MortalFromBlock {
|
||||
for_n_blocks,
|
||||
from_block_n,
|
||||
..
|
||||
} => {
|
||||
Era::mortal(*for_n_blocks, *from_block_n).encode_to(v);
|
||||
}
|
||||
_ => {
|
||||
// Note: if we see `CheckMortalityInner::MortalForBlocks`, then it means the user has
|
||||
// configured a block to be mortal for N blocks, but the current block was never injected,
|
||||
// so we don't know where to start from and default back to building an immortal tx.
|
||||
Era::Immortal.encode_to(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
match &self.params {
|
||||
CheckMortalityParamsInner::MortalFromBlock {
|
||||
from_block_hash, ..
|
||||
} => {
|
||||
from_block_hash.encode_to(v);
|
||||
}
|
||||
_ => {
|
||||
self.genesis_hash.encode_to(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckMortality<T> {
|
||||
type Decoded = Era;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckMortality"
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters to configure the [`CheckMortality`] transaction extension.
|
||||
pub struct CheckMortalityParams<T: Config>(CheckMortalityParamsInner<T>);
|
||||
|
||||
enum CheckMortalityParamsInner<T: Config> {
|
||||
/// The transaction will be immortal.
|
||||
Immortal,
|
||||
/// The transaction is mortal for N blocks. This must be "upgraded" into
|
||||
/// [`CheckMortalityParamsInner::MortalFromBlock`] to ultimately work.
|
||||
MortalForBlocks(u64),
|
||||
/// The transaction is mortal for N blocks, but if it cannot be "upgraded",
|
||||
/// then it will be set to immortal instead. This is the default if unset.
|
||||
MortalForBlocksOrImmortalIfNotPossible(u64),
|
||||
/// The transaction is mortal and all of the relevant information is provided.
|
||||
MortalFromBlock {
|
||||
for_n_blocks: u64,
|
||||
from_block_n: u64,
|
||||
from_block_hash: HashFor<T>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: Config> Default for CheckMortalityParams<T> {
|
||||
fn default() -> Self {
|
||||
// default to being mortal for 32 blocks if possible, else immortal:
|
||||
CheckMortalityParams(CheckMortalityParamsInner::MortalForBlocksOrImmortalIfNotPossible(32))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> CheckMortalityParams<T> {
|
||||
/// Configure a transaction that will be mortal for the number of blocks given.
|
||||
pub fn mortal(for_n_blocks: u64) -> Self {
|
||||
Self(CheckMortalityParamsInner::MortalForBlocks(for_n_blocks))
|
||||
}
|
||||
|
||||
/// Configure a transaction that will be mortal for the number of blocks given,
|
||||
/// and from the block details provided. Prefer to use [`CheckMortalityParams::mortal()`]
|
||||
/// where possible, which prevents the block number and hash from being misaligned.
|
||||
pub fn mortal_from_unchecked(
|
||||
for_n_blocks: u64,
|
||||
from_block_n: u64,
|
||||
from_block_hash: HashFor<T>,
|
||||
) -> Self {
|
||||
Self(CheckMortalityParamsInner::MortalFromBlock {
|
||||
for_n_blocks,
|
||||
from_block_n,
|
||||
from_block_hash,
|
||||
})
|
||||
}
|
||||
/// An immortal transaction.
|
||||
pub fn immortal() -> Self {
|
||||
Self(CheckMortalityParamsInner::Immortal)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for CheckMortalityParams<T> {
|
||||
fn inject_block(&mut self, from_block_n: u64, from_block_hash: HashFor<T>) {
|
||||
match &self.0 {
|
||||
CheckMortalityParamsInner::MortalForBlocks(n)
|
||||
| CheckMortalityParamsInner::MortalForBlocksOrImmortalIfNotPossible(n) => {
|
||||
self.0 = CheckMortalityParamsInner::MortalFromBlock {
|
||||
for_n_blocks: *n,
|
||||
from_block_n,
|
||||
from_block_hash,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Don't change anything if explicit Immortal or explicit block set.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`ChargeAssetTxPayment`] transaction extension.
|
||||
#[derive(DecodeAsType)]
|
||||
#[derive_where(Clone, Debug; T::AssetId)]
|
||||
#[decode_as_type(trait_bounds = "T::AssetId: DecodeAsType")]
|
||||
pub struct ChargeAssetTxPayment<T: Config> {
|
||||
tip: Compact<u128>,
|
||||
asset_id: Option<T::AssetId>,
|
||||
}
|
||||
|
||||
impl<T: Config> ChargeAssetTxPayment<T> {
|
||||
/// Tip to the extrinsic author in the native chain token.
|
||||
pub fn tip(&self) -> u128 {
|
||||
self.tip.0
|
||||
}
|
||||
|
||||
/// Tip to the extrinsic author using the asset ID given.
|
||||
pub fn asset_id(&self) -> Option<&T::AssetId> {
|
||||
self.asset_id.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for ChargeAssetTxPayment<T> {
|
||||
type Params = ChargeAssetTxPaymentParams<T>;
|
||||
|
||||
fn new(_client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(ChargeAssetTxPayment {
|
||||
tip: Compact(params.tip),
|
||||
asset_id: params.asset_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParamsEncoder for ChargeAssetTxPayment<T> {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
(self.tip, &self.asset_id).encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for ChargeAssetTxPayment<T> {
|
||||
type Decoded = Self;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "ChargeAssetTxPayment"
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters to configure the [`ChargeAssetTxPayment`] transaction extension.
|
||||
pub struct ChargeAssetTxPaymentParams<T: Config> {
|
||||
tip: u128,
|
||||
asset_id: Option<T::AssetId>,
|
||||
}
|
||||
|
||||
impl<T: Config> Default for ChargeAssetTxPaymentParams<T> {
|
||||
fn default() -> Self {
|
||||
ChargeAssetTxPaymentParams {
|
||||
tip: Default::default(),
|
||||
asset_id: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ChargeAssetTxPaymentParams<T> {
|
||||
/// Don't provide a tip to the extrinsic author.
|
||||
pub fn no_tip() -> Self {
|
||||
ChargeAssetTxPaymentParams {
|
||||
tip: 0,
|
||||
asset_id: None,
|
||||
}
|
||||
}
|
||||
/// Tip the extrinsic author in the native chain token.
|
||||
pub fn tip(tip: u128) -> Self {
|
||||
ChargeAssetTxPaymentParams {
|
||||
tip,
|
||||
asset_id: None,
|
||||
}
|
||||
}
|
||||
/// Tip the extrinsic author using the asset ID given.
|
||||
pub fn tip_of(tip: u128, asset_id: T::AssetId) -> Self {
|
||||
ChargeAssetTxPaymentParams {
|
||||
tip,
|
||||
asset_id: Some(asset_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for ChargeAssetTxPaymentParams<T> {}
|
||||
|
||||
/// The [`ChargeTransactionPayment`] transaction extension.
|
||||
#[derive(Clone, Debug, DecodeAsType)]
|
||||
pub struct ChargeTransactionPayment {
|
||||
tip: Compact<u128>,
|
||||
}
|
||||
|
||||
impl ChargeTransactionPayment {
|
||||
/// Tip to the extrinsic author in the native chain token.
|
||||
pub fn tip(&self) -> u128 {
|
||||
self.tip.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for ChargeTransactionPayment {
|
||||
type Params = ChargeTransactionPaymentParams;
|
||||
|
||||
fn new(_client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(ChargeTransactionPayment {
|
||||
tip: Compact(params.tip),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for ChargeTransactionPayment {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
self.tip.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for ChargeTransactionPayment {
|
||||
type Decoded = Self;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "ChargeTransactionPayment"
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters to configure the [`ChargeTransactionPayment`] transaction extension.
|
||||
#[derive(Default)]
|
||||
pub struct ChargeTransactionPaymentParams {
|
||||
tip: u128,
|
||||
}
|
||||
|
||||
impl ChargeTransactionPaymentParams {
|
||||
/// Don't provide a tip to the extrinsic author.
|
||||
pub fn no_tip() -> Self {
|
||||
ChargeTransactionPaymentParams { tip: 0 }
|
||||
}
|
||||
/// Tip the extrinsic author in the native chain token.
|
||||
pub fn tip(tip: u128) -> Self {
|
||||
ChargeTransactionPaymentParams { tip }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for ChargeTransactionPaymentParams {}
|
||||
|
||||
/// This accepts a tuple of [`TransactionExtension`]s, and will dynamically make use of whichever
|
||||
/// ones are actually required for the chain in the correct order, ignoring the rest. This
|
||||
/// is a sensible default, and allows for a single configuration to work across multiple chains.
|
||||
pub struct AnyOf<T, Params> {
|
||||
params: Vec<Box<dyn ExtrinsicParamsEncoder + Send + 'static>>,
|
||||
_marker: core::marker::PhantomData<(T, Params)>,
|
||||
}
|
||||
|
||||
macro_rules! impl_tuples {
|
||||
($($ident:ident $index:tt),+) => {
|
||||
// We do some magic when the tuple is wrapped in AnyOf. We
|
||||
// look at the metadata, and use this to select and make use of only the extensions
|
||||
// that we actually need for the chain we're dealing with.
|
||||
impl <T, $($ident),+> ExtrinsicParams<T> for AnyOf<T, ($($ident,)+)>
|
||||
where
|
||||
T: Config,
|
||||
$($ident: TransactionExtension<T>,)+
|
||||
{
|
||||
type Params = ($($ident::Params,)+);
|
||||
|
||||
fn new(
|
||||
client: &ClientState<T>,
|
||||
params: Self::Params,
|
||||
) -> Result<Self, ExtrinsicParamsError> {
|
||||
let metadata = &client.metadata;
|
||||
let types = metadata.types();
|
||||
|
||||
// For each transaction extension in the tuple, find the matching index in the metadata, if
|
||||
// there is one, and add it to a map with that index as the key.
|
||||
let mut exts_by_index = HashMap::new();
|
||||
$({
|
||||
for (idx, e) in metadata.extrinsic().transaction_extensions_to_use_for_encoding().enumerate() {
|
||||
// Skip over any exts that have a match already:
|
||||
if exts_by_index.contains_key(&idx) {
|
||||
continue
|
||||
}
|
||||
// Break and record as soon as we find a match:
|
||||
if $ident::matches(e.identifier(), e.extra_ty(), types) {
|
||||
let ext = $ident::new(client, params.$index)?;
|
||||
let boxed_ext: Box<dyn ExtrinsicParamsEncoder + Send + 'static> = Box::new(ext);
|
||||
exts_by_index.insert(idx, boxed_ext);
|
||||
break
|
||||
}
|
||||
}
|
||||
})+
|
||||
|
||||
// Next, turn these into an ordered vec, erroring if we haven't matched on any exts yet.
|
||||
let mut params = Vec::new();
|
||||
for (idx, e) in metadata.extrinsic().transaction_extensions_to_use_for_encoding().enumerate() {
|
||||
let Some(ext) = exts_by_index.remove(&idx) else {
|
||||
if is_type_empty(e.extra_ty(), types) {
|
||||
continue
|
||||
} else {
|
||||
return Err(ExtrinsicParamsError::UnknownTransactionExtension(e.identifier().to_owned()));
|
||||
}
|
||||
};
|
||||
params.push(ext);
|
||||
}
|
||||
|
||||
Ok(AnyOf {
|
||||
params,
|
||||
_marker: core::marker::PhantomData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl <T, $($ident),+> ExtrinsicParamsEncoder for AnyOf<T, ($($ident,)+)>
|
||||
where
|
||||
T: Config,
|
||||
$($ident: TransactionExtension<T>,)+
|
||||
{
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
for ext in &self.params {
|
||||
ext.encode_value_to(v);
|
||||
}
|
||||
}
|
||||
fn encode_signer_payload_value_to(&self, v: &mut Vec<u8>) {
|
||||
for ext in &self.params {
|
||||
ext.encode_signer_payload_value_to(v);
|
||||
}
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
for ext in &self.params {
|
||||
ext.encode_implicit_to(v);
|
||||
}
|
||||
}
|
||||
fn inject_signature(&mut self, account_id: &dyn Any, signature: &dyn Any) {
|
||||
for ext in &mut self.params {
|
||||
ext.inject_signature(account_id, signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const _: () = {
|
||||
impl_tuples!(A 0);
|
||||
impl_tuples!(A 0, B 1);
|
||||
impl_tuples!(A 0, B 1, C 2);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19, V 20);
|
||||
};
|
||||
|
||||
/// Checks to see whether the type being given is empty, ie would require
|
||||
/// 0 bytes to encode.
|
||||
fn is_type_empty(type_id: u32, types: &scale_info::PortableRegistry) -> bool {
|
||||
let Some(ty) = types.resolve(type_id) else {
|
||||
// Can't resolve; type may not be empty. Not expected to hit this.
|
||||
return false;
|
||||
};
|
||||
|
||||
use scale_info::TypeDef;
|
||||
match &ty.type_def {
|
||||
TypeDef::Composite(c) => c.fields.iter().all(|f| is_type_empty(f.ty.id, types)),
|
||||
TypeDef::Array(a) => a.len == 0 || is_type_empty(a.type_param.id, types),
|
||||
TypeDef::Tuple(t) => t.fields.iter().all(|f| is_type_empty(f.id, types)),
|
||||
// Explicitly list these in case any additions are made in the future.
|
||||
TypeDef::BitSequence(_)
|
||||
| TypeDef::Variant(_)
|
||||
| TypeDef::Sequence(_)
|
||||
| TypeDef::Compact(_)
|
||||
| TypeDef::Primitive(_) => false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use crate::client::OfflineClientAtBlockT;
|
||||
use crate::config::Config;
|
||||
use crate::error::ConstantError;
|
||||
use address::Address;
|
||||
use frame_decode::constants::ConstantTypeInfo;
|
||||
use scale_decode::IntoVisitor;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub mod address;
|
||||
|
||||
/// A client for working with storage entries.
|
||||
#[derive(Clone)]
|
||||
pub struct ConstantsClient<'atblock, T, Client> {
|
||||
client: &'atblock Client,
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'atblock, T, Client> ConstantsClient<'atblock, T, Client> {
|
||||
pub(crate) fn new(client: &'atblock Client) -> Self {
|
||||
ConstantsClient {
|
||||
client,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'atblock, T: Config, Client: OfflineClientAtBlockT<T>> ConstantsClient<'atblock, 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_ref();
|
||||
if let Some(actual_hash) = address.validation_hash() {
|
||||
let expected_hash = metadata
|
||||
.pallet_by_name(address.pallet_name())
|
||||
.ok_or_else(|| {
|
||||
ConstantError::PalletNameNotFound(address.pallet_name().to_string())
|
||||
})?
|
||||
.constant_hash(address.constant_name())
|
||||
.ok_or_else(|| ConstantError::ConstantNameNotFound {
|
||||
pallet_name: address.pallet_name().to_string(),
|
||||
constant_name: address.constant_name().to_owned(),
|
||||
})?;
|
||||
if actual_hash != expected_hash {
|
||||
return Err(ConstantError::IncompatibleCodegen);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Access the constant at the given address, returning the value defined by this address.
|
||||
pub fn entry<Addr: Address>(&self, address: Addr) -> Result<Addr::Target, ConstantError> {
|
||||
let metadata = self.client.metadata_ref();
|
||||
|
||||
// 1. Validate constant shape if hash given:
|
||||
self.validate(&address)?;
|
||||
|
||||
// 2. Attempt to decode the constant into the type given:
|
||||
let constant = frame_decode::constants::decode_constant(
|
||||
address.pallet_name(),
|
||||
address.constant_name(),
|
||||
metadata,
|
||||
metadata.types(),
|
||||
Addr::Target::into_visitor(),
|
||||
)
|
||||
.map_err(ConstantError::CouldNotDecodeConstant)?;
|
||||
|
||||
Ok(constant)
|
||||
}
|
||||
|
||||
/// Access the bytes of a constant by its address.
|
||||
pub fn entry_bytes<Addr: Address>(&self, address: Addr) -> Result<Vec<u8>, ConstantError> {
|
||||
// 1. Validate custom value shape if hash given:
|
||||
self.validate(&address)?;
|
||||
|
||||
// 2. Return the underlying bytes:
|
||||
let constant = self
|
||||
.client
|
||||
.metadata_ref()
|
||||
.constant_info(address.pallet_name(), address.constant_name())
|
||||
.map_err(|e| ConstantError::ConstantInfoError(e.into_owned()))?;
|
||||
Ok(constant.bytes.to_vec())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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.
|
||||
|
||||
//! Construct addresses to access constants with.
|
||||
|
||||
use derive_where::derive_where;
|
||||
use scale_decode::DecodeAsType;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// This represents a constant address. Anything implementing this trait
|
||||
/// can be used to fetch constants.
|
||||
pub trait Address {
|
||||
/// The target type of the value that lives at this address.
|
||||
type Target: DecodeAsType;
|
||||
|
||||
/// The name of the pallet that the constant lives under.
|
||||
fn pallet_name(&self) -> &str;
|
||||
|
||||
/// The name of the constant in a given pallet.
|
||||
fn constant_name(&self) -> &str;
|
||||
|
||||
/// An optional hash which, if present, will be checked against
|
||||
/// the node metadata to confirm that the return type matches what
|
||||
/// we are expecting.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Any reference to an address is a valid address.
|
||||
impl<A: Address + ?Sized> Address for &'_ A {
|
||||
type Target = A::Target;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
A::pallet_name(*self)
|
||||
}
|
||||
|
||||
fn constant_name(&self) -> &str {
|
||||
A::constant_name(*self)
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
A::validation_hash(*self)
|
||||
}
|
||||
}
|
||||
|
||||
// (str, str) and similar are valid addresses.
|
||||
impl<A: AsRef<str>, B: AsRef<str>> Address for (A, B) {
|
||||
type Target = scale_value::Value;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
||||
fn constant_name(&self) -> &str {
|
||||
self.1.as_ref()
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents the address of a constant.
|
||||
#[derive_where(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub struct StaticAddress<ReturnTy> {
|
||||
pallet_name: Cow<'static, str>,
|
||||
constant_name: Cow<'static, str>,
|
||||
constant_hash: Option<[u8; 32]>,
|
||||
_marker: core::marker::PhantomData<ReturnTy>,
|
||||
}
|
||||
|
||||
/// A dynamic lookup address to access a constant.
|
||||
pub type DynamicAddress<ReturnTy> = StaticAddress<ReturnTy>;
|
||||
|
||||
impl<ReturnTy> StaticAddress<ReturnTy> {
|
||||
/// Create a new [`StaticAddress`] to use to look up a constant.
|
||||
pub fn new(pallet_name: impl Into<String>, constant_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
pallet_name: Cow::Owned(pallet_name.into()),
|
||||
constant_name: Cow::Owned(constant_name.into()),
|
||||
constant_hash: None,
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`StaticAddress`] that will be validated
|
||||
/// against node metadata using the hash given.
|
||||
#[doc(hidden)]
|
||||
pub fn new_static(
|
||||
pallet_name: &'static str,
|
||||
constant_name: &'static str,
|
||||
hash: [u8; 32],
|
||||
) -> Self {
|
||||
Self {
|
||||
pallet_name: Cow::Borrowed(pallet_name),
|
||||
constant_name: Cow::Borrowed(constant_name),
|
||||
constant_hash: Some(hash),
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this constant prior to accessing it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
pallet_name: self.pallet_name,
|
||||
constant_name: self.constant_name,
|
||||
constant_hash: None,
|
||||
_marker: self._marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ReturnTy: DecodeAsType> Address for StaticAddress<ReturnTy> {
|
||||
type Target = ReturnTy;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
&self.pallet_name
|
||||
}
|
||||
|
||||
fn constant_name(&self) -> &str {
|
||||
&self.constant_name
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.constant_hash
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new dynamic constant lookup.
|
||||
pub fn dynamic<ReturnTy: DecodeAsType>(
|
||||
pallet_name: impl Into<String>,
|
||||
constant_name: impl Into<String>,
|
||||
) -> DynamicAddress<ReturnTy> {
|
||||
DynamicAddress::new(pallet_name, constant_name)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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 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();
|
||||
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();
|
||||
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();
|
||||
subxt_core::constants::get_bytes(address, &metadata)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// 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 subxt_core::constants::address::{Address, DynamicAddress, StaticAddress, dynamic};
|
||||
@@ -0,0 +1,85 @@
|
||||
use crate::client::OfflineClientAtBlockT;
|
||||
use crate::config::Config;
|
||||
use crate::error::CustomValueError;
|
||||
use crate::utils::Maybe;
|
||||
use address::Address;
|
||||
use derive_where::derive_where;
|
||||
use frame_decode::custom_values::CustomValueTypeInfo;
|
||||
use scale_decode::IntoVisitor;
|
||||
|
||||
pub mod address;
|
||||
|
||||
/// A client for accessing custom values stored in the metadata.
|
||||
#[derive_where(Clone; Client)]
|
||||
pub struct CustomValuesClient<'atblock, T, Client> {
|
||||
client: &'atblock Client,
|
||||
marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'atblock, T, Client> CustomValuesClient<'atblock, T, Client> {
|
||||
/// Create a new [`CustomValuesClient`].
|
||||
pub(crate) fn new(client: &'atblock Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'atblock, T: Config, Client: OfflineClientAtBlockT<T>>
|
||||
CustomValuesClient<'atblock, T, Client>
|
||||
{
|
||||
/// 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> {
|
||||
let metadata = self.client.metadata_ref();
|
||||
if let Some(actual_hash) = address.validation_hash() {
|
||||
let custom = metadata.custom();
|
||||
let custom_value = custom
|
||||
.get(address.name())
|
||||
.ok_or_else(|| CustomValueError::NotFound(address.name().into()))?;
|
||||
let expected_hash = custom_value.hash();
|
||||
if actual_hash != expected_hash {
|
||||
return Err(CustomValueError::IncompatibleCodegen);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 entry<Addr: Address<IsDecodable = Maybe>>(
|
||||
&self,
|
||||
address: Addr,
|
||||
) -> Result<Addr::Target, CustomValueError> {
|
||||
// 1. Validate custom value shape if hash given:
|
||||
self.validate(&address)?;
|
||||
|
||||
// 2. Attempt to decode custom value:
|
||||
let metadata = self.client.metadata_ref();
|
||||
let value = frame_decode::custom_values::decode_custom_value(
|
||||
address.name(),
|
||||
metadata,
|
||||
metadata.types(),
|
||||
Addr::Target::into_visitor(),
|
||||
)
|
||||
.map_err(CustomValueError::CouldNotDecodeCustomValue)?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Access the bytes of a custom value by the address it is registered under.
|
||||
pub fn entry_bytes<Addr: Address>(&self, address: Addr) -> Result<Vec<u8>, CustomValueError> {
|
||||
// 1. Validate custom value shape if hash given:
|
||||
self.validate(&address)?;
|
||||
|
||||
// 2. Return the underlying bytes:
|
||||
let custom_value = self
|
||||
.client
|
||||
.metadata_ref()
|
||||
.custom_value_info(address.name())
|
||||
.map_err(|e| CustomValueError::NotFound(e.not_found))?;
|
||||
Ok(custom_value.bytes.to_vec())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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.
|
||||
|
||||
//! Construct addresses to access custom values with.
|
||||
|
||||
use derive_where::derive_where;
|
||||
use scale_decode::DecodeAsType;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Use this with [`Address::IsDecodable`].
|
||||
pub use crate::utils::{Maybe, No, NoMaybe};
|
||||
|
||||
/// This represents the address of a custom value in the metadata.
|
||||
/// Anything that implements it can be used to fetch custom values from the metadata.
|
||||
/// The trait is implemented by [`str`] for dynamic lookup and [`StaticAddress`] for static queries.
|
||||
pub trait Address {
|
||||
/// The type of the custom value.
|
||||
type Target: DecodeAsType;
|
||||
/// Should be set to `Yes` for Dynamic values and static values that have a valid type.
|
||||
/// Should be `No` for custom values, that have an invalid type id.
|
||||
type IsDecodable: NoMaybe;
|
||||
|
||||
/// the name (key) by which the custom value can be accessed in the metadata.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// An optional hash which, if present, can be checked against node metadata.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Any reference to an address is a valid address
|
||||
impl<A: Address + ?Sized> Address for &'_ A {
|
||||
type Target = A::Target;
|
||||
type IsDecodable = A::IsDecodable;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
A::name(*self)
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
A::validation_hash(*self)
|
||||
}
|
||||
}
|
||||
|
||||
// Support plain strings for looking up custom values.
|
||||
impl Address for str {
|
||||
type Target = scale_value::Value;
|
||||
type IsDecodable = Maybe;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A static address to a custom value.
|
||||
#[derive_where(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub struct StaticAddress<ReturnTy, IsDecodable> {
|
||||
name: Cow<'static, str>,
|
||||
hash: Option<[u8; 32]>,
|
||||
marker: core::marker::PhantomData<(ReturnTy, IsDecodable)>,
|
||||
}
|
||||
|
||||
/// A dynamic address to a custom value.
|
||||
pub type DynamicAddress<ReturnTy> = StaticAddress<ReturnTy, Maybe>;
|
||||
|
||||
impl<ReturnTy, IsDecodable> StaticAddress<ReturnTy, IsDecodable> {
|
||||
#[doc(hidden)]
|
||||
/// Creates a new StaticAddress.
|
||||
pub fn new_static(name: &'static str, hash: [u8; 32]) -> Self {
|
||||
Self {
|
||||
name: Cow::Borrowed(name),
|
||||
hash: Some(hash),
|
||||
marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`StaticAddress`]
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into().into(),
|
||||
hash: None,
|
||||
marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this custom value prior to accessing it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
name: self.name,
|
||||
hash: None,
|
||||
marker: self.marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Target: DecodeAsType, IsDecodable: NoMaybe> Address for StaticAddress<Target, IsDecodable> {
|
||||
type Target = Target;
|
||||
type IsDecodable = IsDecodable;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.hash
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new dynamic custom value lookup.
|
||||
pub fn dynamic<ReturnTy: DecodeAsType>(
|
||||
custom_value_name: impl Into<String>,
|
||||
) -> DynamicAddress<ReturnTy> {
|
||||
DynamicAddress::new(custom_value_name)
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
use crate::client::OfflineClientT;
|
||||
use crate::{Config, error::CustomValueError};
|
||||
use derive_where::derive_where;
|
||||
|
||||
use 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> {
|
||||
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> {
|
||||
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> {
|
||||
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 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: 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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// 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 subxt_core::custom_values::address::{Address, DynamicAddress, StaticAddress, dynamic};
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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.
|
||||
|
||||
//! This module ex-exports various helpers for constructing dynamic payloads/queries/addresses.
|
||||
|
||||
pub use scale_value::{At, Value, value};
|
||||
|
||||
// Submit dynamic transactions.
|
||||
pub use crate::transactions::payload::dynamic as transaction;
|
||||
|
||||
// Lookup constants dynamically.
|
||||
pub use crate::constants::address::dynamic as constant;
|
||||
|
||||
// Lookup storage values dynamically.
|
||||
pub use crate::storage::address::dynamic as storage;
|
||||
|
||||
// Execute runtime API function call dynamically.
|
||||
pub use crate::runtime_apis::payload::dynamic as runtime_api_call;
|
||||
|
||||
// Execute View Function API function call dynamically.
|
||||
pub use crate::view_functions::payload::dynamic as view_function_call;
|
||||
|
||||
/// Obtain a custom value from the metadata.
|
||||
pub use crate::custom_values::address::dynamic as custom_value;
|
||||
@@ -7,9 +7,11 @@
|
||||
mod dispatch_error;
|
||||
mod hex;
|
||||
|
||||
crate::macros::cfg_unstable_light_client! {
|
||||
pub use subxt_lightclient::LightClientError;
|
||||
}
|
||||
use std::borrow::Cow;
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
#[cfg(feature = "unstable-light-client")]
|
||||
pub use subxt_lightclient::LightClientError;
|
||||
|
||||
// Re-export dispatch error types:
|
||||
pub use dispatch_error::{
|
||||
@@ -17,34 +19,24 @@ pub use dispatch_error::{
|
||||
};
|
||||
|
||||
// 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 subxt_metadata::Metadata;
|
||||
pub use subxt_metadata::TryFromError as MetadataTryFromError;
|
||||
|
||||
// Re-export core error types we're just reusing.
|
||||
pub use 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)]
|
||||
OnlineClientError(#[from] OnlineClientError),
|
||||
#[error(transparent)]
|
||||
OfflineClientAtBlockError(#[from] OfflineClientAtBlockError),
|
||||
#[error(transparent)]
|
||||
OnlineClientAtBlockError(#[from] OnlineClientAtBlockError),
|
||||
#[error(transparent)]
|
||||
ExtrinsicDecodeErrorAt(#[from] ExtrinsicDecodeErrorAt),
|
||||
#[error(transparent)]
|
||||
@@ -58,16 +50,10 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
BackendError(#[from] BackendError),
|
||||
#[error(transparent)]
|
||||
BlockError(#[from] BlockError),
|
||||
BlocksError(#[from] BlocksError),
|
||||
#[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),
|
||||
@@ -153,10 +139,12 @@ impl Error {
|
||||
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
Error::BlockError(e) => e.backend_error(),
|
||||
// Many of these contain no backend error, but keep the checks next to
|
||||
// the actual error types to make it harder to miss adding any, and be exhaustive
|
||||
// here so new error variants are not missed as easily.
|
||||
Error::BlocksError(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(),
|
||||
@@ -165,130 +153,51 @@ impl 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,
|
||||
Error::OfflineClientAtBlockError(e) => e.backend_error(),
|
||||
Error::OnlineClientAtBlockError(e) => e.backend_error(),
|
||||
Error::ExtrinsicDecodeErrorAt(e) => e.backend_error(),
|
||||
Error::ConstantError(e) => e.backend_error(),
|
||||
Error::CustomValueError(e) => e.backend_error(),
|
||||
Error::StorageKeyError(e) => e.backend_error(),
|
||||
Error::StorageValueError(e) => e.backend_error(),
|
||||
Error::TransactionStatusError(e) => e.backend_error(),
|
||||
Error::ModuleErrorDetailsError(e) => e.backend_error(),
|
||||
Error::ModuleErrorDecodeError(e) => e.backend_error(),
|
||||
Error::DispatchErrorDecodeError(e) => e.backend_error(),
|
||||
// BackendError is always a BackendError:
|
||||
Error::BackendError(e) => Some(e),
|
||||
// Other errors come from different crates so can never contain a BackendError:
|
||||
Error::OtherRpcClientError(_) => None,
|
||||
Error::OtherCodecError(_) => None,
|
||||
Error::Other(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
/// Errors constructing an offline client at a specific block number.
|
||||
#[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(
|
||||
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<subxt_rpcs::Error> for BackendError {
|
||||
fn from(value: 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] subxt_rpcs::Error),
|
||||
/// This error signals that we got back a [`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 {
|
||||
pub enum OfflineClientAtBlockError {
|
||||
#[error(
|
||||
"Could not find the block body with hash {block_hash} (perhaps it was on a non-finalized fork?)"
|
||||
"Cannot construct OfflineClientAtBlock: spec version not found for block number {block_number}"
|
||||
)]
|
||||
BlockNotFound { block_hash: Hex },
|
||||
#[error("Could not download the block header with hash {block_hash}: {reason}")]
|
||||
CouldNotGetBlockHeader {
|
||||
block_hash: Hex,
|
||||
reason: BackendError,
|
||||
SpecVersionNotFound {
|
||||
/// The block number for which the spec version was not found.
|
||||
block_number: u64,
|
||||
},
|
||||
#[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,
|
||||
#[error(
|
||||
"Cannot construct OfflineClientAtBlock: metadata not found for spec version {spec_version}"
|
||||
)]
|
||||
MetadataNotFound {
|
||||
/// The spec version for which the metadata was not found.
|
||||
spec_version: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl BlockError {
|
||||
impl OfflineClientAtBlockError {
|
||||
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,
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,8 +205,15 @@ impl AccountNonceError {
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum OnlineClientError {
|
||||
#[error("Cannot construct OnlineClient: The URL provided is invalid: {url}")]
|
||||
InvalidUrl {
|
||||
/// The URL that was invalid.
|
||||
url: String,
|
||||
},
|
||||
#[error("Cannot construct OnlineClient: {0}")]
|
||||
RpcError(#[from] subxt_rpcs::Error),
|
||||
#[error("Could not construct the CombinedBackend: {0}")]
|
||||
CannotBuildCombinedBackend(CombinedBackendError),
|
||||
#[error(
|
||||
"Cannot construct OnlineClient: Cannot fetch latest finalized block to obtain init details from: {0}"
|
||||
)]
|
||||
@@ -322,54 +238,241 @@ impl OnlineClientError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors constructing streams of blocks.
|
||||
#[allow(missing_docs)]
|
||||
#[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),
|
||||
pub enum BlocksError {
|
||||
#[error("Cannot construct block stream: cannot get the current block: {0}")]
|
||||
CannotGetCurrentBlock(OnlineClientAtBlockError),
|
||||
#[error("Cannot construct block stream: cannot get block header stream: {0}")]
|
||||
CannotGetBlockHeaderStream(BackendError),
|
||||
#[error("Error streaming blocks: cannot get the next block header: {0}")]
|
||||
CannotGetBlockHeader(BackendError),
|
||||
}
|
||||
|
||||
impl RuntimeUpdaterError {
|
||||
impl BlocksError {
|
||||
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),
|
||||
BlocksError::CannotGetCurrentBlock(e) => e.backend_error(),
|
||||
BlocksError::CannotGetBlockHeaderStream(e) => Some(e),
|
||||
BlocksError::CannotGetBlockHeader(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors constructing an online client at a specific block number.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum OnlineClientAtBlockError {
|
||||
#[error("Cannot construct OnlineClientAtBlock: cannot get the current block: {reason}")]
|
||||
CannotGetCurrentBlock {
|
||||
/// The error we encountered.
|
||||
reason: BackendError,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: failed to get block hash from node for block {block_number}: {reason}"
|
||||
)]
|
||||
CannotGetBlockHash {
|
||||
/// Block number we failed to get the hash for.
|
||||
block_number: u64,
|
||||
/// The error we encountered.
|
||||
reason: BackendError,
|
||||
},
|
||||
#[error("Cannot construct OnlineClientAtBlock: block number {block_number} not found")]
|
||||
BlockNotFound {
|
||||
/// The block number for which a block was not found.
|
||||
block_number: u64,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: cannot get the block header for block {block_hash}: {reason}"
|
||||
)]
|
||||
CannotGetBlockHeader {
|
||||
/// Block hash that we failed to fetch the header for.
|
||||
block_hash: Hex,
|
||||
/// The error we encountered.
|
||||
reason: BackendError,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: cannot find the block header for block {block_hash}"
|
||||
)]
|
||||
BlockHeaderNotFound {
|
||||
/// Block hash that we failed to find the header for.
|
||||
block_hash: Hex,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: failed to obtain spec version for block {block_hash}: {reason}"
|
||||
)]
|
||||
CannotGetSpecVersion {
|
||||
/// The block hash for which we failed to obtain the spec version.
|
||||
block_hash: Hex,
|
||||
/// The error we encountered.
|
||||
reason: BackendError,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: failed to decode spec version for block {block_hash}: {reason}"
|
||||
)]
|
||||
CannotDecodeSpecVersion {
|
||||
/// The block hash for which we failed to decode the spec version.
|
||||
block_hash: Hex,
|
||||
/// The error we encountered.
|
||||
reason: codec::Error,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: failed to get metadata for block {block_hash}: {reason}"
|
||||
)]
|
||||
CannotGetMetadata {
|
||||
/// The block hash for which we failed to get the metadata.
|
||||
block_hash: Hex,
|
||||
/// The error we encountered.
|
||||
reason: String,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: Metadata V{version} (required at block {block_hash} is not supported."
|
||||
)]
|
||||
UnsupportedMetadataVersion {
|
||||
/// The block hash that requires the unsupported version.
|
||||
block_hash: Hex,
|
||||
/// The unsupported metadata version.
|
||||
version: u32,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: No legacy types were provided but we're trying to access a block that requires them."
|
||||
)]
|
||||
MissingLegacyTypes,
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: unable to convert legacy metadata (required at block {block_hash}): {reason}"
|
||||
)]
|
||||
CannotConvertLegacyMetadata {
|
||||
/// The block hash that requires legacy types.
|
||||
block_hash: Hex,
|
||||
/// The metadata version.
|
||||
metadata_version: u32,
|
||||
/// Reason the conversion failed.
|
||||
reason: subxt_metadata::LegacyFromError,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: unable to convert modern metadata (required at block {block_hash}): {reason}"
|
||||
)]
|
||||
CannotConvertModernMetadata {
|
||||
/// The block hash that requires legacy types.
|
||||
block_hash: Hex,
|
||||
/// The metadata version.
|
||||
metadata_version: u32,
|
||||
/// Reason the conversion failed.
|
||||
reason: subxt_metadata::TryFromError,
|
||||
},
|
||||
#[error(
|
||||
"Cannot construct OnlineClientAtBlock: cannot inject types from metadata: failure to parse a type found in the metadata: {parse_error}"
|
||||
)]
|
||||
CannotInjectMetadataTypes {
|
||||
/// Error parsing a type found in the metadata.
|
||||
parse_error: scale_info_legacy::lookup_name::ParseError,
|
||||
},
|
||||
}
|
||||
|
||||
impl OnlineClientAtBlockError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
match self {
|
||||
OnlineClientAtBlockError::CannotGetCurrentBlock { reason }
|
||||
| OnlineClientAtBlockError::CannotGetBlockHash { reason, .. }
|
||||
| OnlineClientAtBlockError::CannotGetBlockHeader { reason, .. }
|
||||
| OnlineClientAtBlockError::CannotGetSpecVersion { reason, .. } => Some(reason),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error that can occur during upgrade.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum RuntimeUpdateeApplyError {
|
||||
#[error("The proposed runtime update is the same as the current version")]
|
||||
SameVersion,
|
||||
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(Cow<'static, str>),
|
||||
}
|
||||
|
||||
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(
|
||||
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))
|
||||
}
|
||||
|
||||
/// Create a [`BackendError::Other`] given a message.
|
||||
pub fn other(message: impl Into<Cow<'static, str>>) -> Self {
|
||||
BackendError::Other(message.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<subxt_rpcs::Error> for BackendError {
|
||||
fn from(value: subxt_rpcs::Error) -> Self {
|
||||
BackendError::Rpc(RpcError::ClientError(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum CombinedBackendError {
|
||||
#[error("Could not obtain the list of RPC methods to determine which backends can be used")]
|
||||
CouldNotObtainRpcMethodList(subxt_rpcs::Error),
|
||||
}
|
||||
|
||||
/// 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] subxt_rpcs::Error),
|
||||
/// This error signals that we got back a [`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,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error working with Runtime APIs
|
||||
@@ -377,10 +480,21 @@ pub enum RuntimeUpdateeApplyError {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum RuntimeApiError {
|
||||
#[error("The static Runtime API address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Runtime API trait not found: {0}")]
|
||||
TraitNotFound(String),
|
||||
#[error("Runtime API method {method_name} not found in trait {trait_name}")]
|
||||
MethodNotFound {
|
||||
trait_name: String,
|
||||
method_name: String,
|
||||
},
|
||||
#[error("Failed to encode Runtime API inputs: {0}")]
|
||||
CouldNotEncodeInputs(frame_decode::runtime_apis::RuntimeApiInputsEncodeError),
|
||||
#[error("Failed to decode Runtime API: {0}")]
|
||||
CouldNotDecodeResponse(frame_decode::runtime_apis::RuntimeApiDecodeError<u32>),
|
||||
#[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),
|
||||
}
|
||||
@@ -400,8 +514,42 @@ impl RuntimeApiError {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum EventsError {
|
||||
#[error("{0}")]
|
||||
OfflineError(#[from] CoreEventsError),
|
||||
#[error("Can't decode event: can't decode phase: {0}")]
|
||||
CannotDecodePhase(codec::Error),
|
||||
#[error("Can't decode event: can't decode pallet index: {0}")]
|
||||
CannotDecodePalletIndex(codec::Error),
|
||||
#[error("Can't decode event: can't decode variant index: {0}")]
|
||||
CannotDecodeVariantIndex(codec::Error),
|
||||
#[error("Can't decode event: can't find pallet with index {0}")]
|
||||
CannotFindPalletWithIndex(u8),
|
||||
#[error(
|
||||
"Can't decode event: can't find variant with index {variant_index} in pallet {pallet_name}"
|
||||
)]
|
||||
CannotFindVariantWithIndex {
|
||||
pallet_name: String,
|
||||
variant_index: u8,
|
||||
},
|
||||
#[error("Can't decode field {field_name:?} in event {pallet_name}.{event_name}: {reason}")]
|
||||
CannotDecodeFieldInEvent {
|
||||
pallet_name: String,
|
||||
event_name: String,
|
||||
field_name: String,
|
||||
reason: scale_decode::visitor::DecodeError,
|
||||
},
|
||||
#[error("Can't decode event topics: {0}")]
|
||||
CannotDecodeEventTopics(codec::Error),
|
||||
#[error("Can't decode the fields of event {pallet_name}.{event_name}: {reason}")]
|
||||
CannotDecodeEventFields {
|
||||
pallet_name: String,
|
||||
event_name: String,
|
||||
reason: scale_decode::Error,
|
||||
},
|
||||
#[error("Can't decode event {pallet_name}.{event_name} to Event enum: {reason}")]
|
||||
CannotDecodeEventEnum {
|
||||
pallet_name: String,
|
||||
event_name: String,
|
||||
reason: scale_decode::Error,
|
||||
},
|
||||
#[error("Cannot access events at latest block: Cannot fetch latest finalized block: {0}")]
|
||||
CannotGetLatestFinalizedBlock(BackendError),
|
||||
#[error("Cannot fetch event bytes: {0}")]
|
||||
@@ -423,14 +571,59 @@ impl EventsError {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ExtrinsicError {
|
||||
#[error("The extrinsic payload is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Can't find extrinsic: pallet with name {0} not found")]
|
||||
PalletNameNotFound(String),
|
||||
#[error("Can't find extrinsic: call name {call_name} doesn't exist in pallet {pallet_name}")]
|
||||
CallNameNotFound {
|
||||
pallet_name: String,
|
||||
call_name: String,
|
||||
},
|
||||
#[error("Can't encode the extrinsic call data: {0}")]
|
||||
CannotEncodeCallData(scale_encode::Error),
|
||||
#[error("Failed to encode an extrinsic: the genesis hash was not provided")]
|
||||
GenesisHashNotProvided,
|
||||
#[error("Subxt does not support the extrinsic versions expected by the chain")]
|
||||
UnsupportedVersion,
|
||||
#[error("Cannot construct the required transaction extensions: {0}")]
|
||||
Params(#[from] ExtrinsicParamsError),
|
||||
#[error("Cannot decode transaction extension '{name}': {error}")]
|
||||
CouldNotDecodeTransactionExtension {
|
||||
/// The extension name.
|
||||
name: String,
|
||||
/// The decode error.
|
||||
error: scale_decode::Error,
|
||||
},
|
||||
#[error(
|
||||
"After decoding the extrinsic at index {extrinsic_index}, {num_leftover_bytes} bytes were left, suggesting that decoding may have failed"
|
||||
)]
|
||||
LeftoverBytes {
|
||||
/// Index of the extrinsic that failed to decode.
|
||||
extrinsic_index: usize,
|
||||
/// Number of bytes leftover after decoding the extrinsic.
|
||||
num_leftover_bytes: usize,
|
||||
},
|
||||
#[error("{0}")]
|
||||
OfflineError(#[from] CoreExtrinsicError),
|
||||
ExtrinsicDecodeErrorAt(#[from] ExtrinsicDecodeErrorAt),
|
||||
#[error("Failed to decode the fields of an extrinsic at index {extrinsic_index}: {error}")]
|
||||
CannotDecodeFields {
|
||||
/// Index of the extrinsic whose fields we could not decode
|
||||
extrinsic_index: usize,
|
||||
/// The decode error.
|
||||
error: scale_decode::Error,
|
||||
},
|
||||
#[error("Failed to decode the extrinsic at index {extrinsic_index} to a root enum: {error}")]
|
||||
CannotDecodeIntoRootExtrinsic {
|
||||
/// Index of the extrinsic that we failed to decode
|
||||
extrinsic_index: usize,
|
||||
/// The decode error.
|
||||
error: scale_decode::Error,
|
||||
},
|
||||
#[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}"
|
||||
)]
|
||||
@@ -480,13 +673,42 @@ impl ExtrinsicError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum CustomValueError {
|
||||
#[error("The static custom value address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("The custom value '{0}' was not found")]
|
||||
NotFound(String),
|
||||
#[error("Failed to decode custom value: {0}")]
|
||||
CouldNotDecodeCustomValue(frame_decode::custom_values::CustomValueDecodeError<u32>),
|
||||
}
|
||||
|
||||
impl CustomValueError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Error working with View Functions.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ViewFunctionError {
|
||||
#[error("{0}")]
|
||||
OfflineError(#[from] CoreViewFunctionError),
|
||||
#[error("The static View Function address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Can't find View Function: pallet {0} not found")]
|
||||
PalletNotFound(String),
|
||||
#[error("Can't find View Function {function_name} in pallet {pallet_name}")]
|
||||
ViewFunctionNotFound {
|
||||
pallet_name: String,
|
||||
function_name: String,
|
||||
},
|
||||
#[error("Failed to encode View Function inputs: {0}")]
|
||||
CouldNotEncodeInputs(frame_decode::view_functions::ViewFunctionInputsEncodeError),
|
||||
#[error("Failed to decode View Function: {0}")]
|
||||
CouldNotDecodeResponse(frame_decode::view_functions::ViewFunctionDecodeError<u32>),
|
||||
#[error(
|
||||
"Cannot access View Functions at latest block: Cannot fetch latest finalized block: {0}"
|
||||
)]
|
||||
@@ -546,6 +768,12 @@ pub enum TransactionStatusError {
|
||||
Dropped(String),
|
||||
}
|
||||
|
||||
impl TransactionStatusError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Error fetching events for a just-submitted transaction
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
@@ -575,6 +803,8 @@ pub enum TransactionEventsError {
|
||||
block_hash: Hex,
|
||||
error: EventsError,
|
||||
},
|
||||
#[error("Could not instantiate a client at the required block to fetch events: {0}")]
|
||||
CannotInstantiateClientAtBlock(OnlineClientAtBlockError),
|
||||
#[error("Could not fetch events for the submitted transaction: {error}")]
|
||||
CannotFetchEventsForTransaction {
|
||||
block_hash: Hex,
|
||||
@@ -641,6 +871,12 @@ pub enum ModuleErrorDetailsError {
|
||||
},
|
||||
}
|
||||
|
||||
impl ModuleErrorDetailsError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Error decoding the [`ModuleError`]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
@@ -648,6 +884,12 @@ pub enum ModuleErrorDetailsError {
|
||||
#[error("Could not decode the DispatchError::Module payload into the given type: {0}")]
|
||||
pub struct ModuleErrorDecodeError(scale_decode::Error);
|
||||
|
||||
impl ModuleErrorDecodeError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Error decoding the [`DispatchError`]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
@@ -666,13 +908,45 @@ pub enum DispatchErrorDecodeError {
|
||||
},
|
||||
}
|
||||
|
||||
impl DispatchErrorDecodeError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Error working with storage.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum StorageError {
|
||||
#[error("{0}")]
|
||||
Offline(#[from] CoreStorageError),
|
||||
#[error("The static storage address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Can't find storage value: pallet with name {0} not found")]
|
||||
PalletNameNotFound(String),
|
||||
#[error(
|
||||
"Storage entry '{entry_name}' not found in pallet {pallet_name} in the live chain metadata"
|
||||
)]
|
||||
StorageEntryNotFound {
|
||||
pallet_name: String,
|
||||
entry_name: String,
|
||||
},
|
||||
#[error("Cannot obtain storage information from metadata: {0}")]
|
||||
StorageInfoError(frame_decode::storage::StorageInfoError<'static>),
|
||||
#[error("Cannot encode storage key: {0}")]
|
||||
StorageKeyEncodeError(frame_decode::storage::StorageKeyEncodeError),
|
||||
#[error("Cannot create a key to iterate over a plain entry")]
|
||||
CannotIterPlainEntry {
|
||||
pallet_name: String,
|
||||
entry_name: String,
|
||||
},
|
||||
#[error(
|
||||
"Wrong number of key parts provided to iterate a storage address. We expected at most {max_expected} key parts but got {got} key parts"
|
||||
)]
|
||||
WrongNumberOfKeyPartsProvidedForIterating { max_expected: usize, got: usize },
|
||||
#[error(
|
||||
"Wrong number of key parts provided to fetch a storage address. We expected {expected} key parts but got {got} key parts"
|
||||
)]
|
||||
WrongNumberOfKeyPartsProvidedForFetching { expected: usize, got: usize },
|
||||
#[error("Cannot access storage at latest block: Cannot fetch latest finalized block: {0}")]
|
||||
CannotGetLatestFinalizedBlock(BackendError),
|
||||
#[error(
|
||||
@@ -700,3 +974,136 @@ impl StorageError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Something went wrong working with a constant.
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ConstantError {
|
||||
#[error("The static constant address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Can't find constant: pallet with name {0} not found")]
|
||||
PalletNameNotFound(String),
|
||||
#[error(
|
||||
"Constant '{constant_name}' not found in pallet {pallet_name} in the live chain metadata"
|
||||
)]
|
||||
ConstantNameNotFound {
|
||||
pallet_name: String,
|
||||
constant_name: String,
|
||||
},
|
||||
#[error("Failed to decode constant: {0}")]
|
||||
CouldNotDecodeConstant(frame_decode::constants::ConstantDecodeError<u32>),
|
||||
#[error("Cannot obtain constant information from metadata: {0}")]
|
||||
ConstantInfoError(frame_decode::constants::ConstantInfoError<'static>),
|
||||
}
|
||||
|
||||
impl ConstantError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum StorageKeyError {
|
||||
#[error("Can't decode the storage key: {error}")]
|
||||
StorageKeyDecodeError {
|
||||
bytes: Vec<u8>,
|
||||
error: frame_decode::storage::StorageKeyDecodeError<u32>,
|
||||
},
|
||||
#[error("Can't decode the values from the storage key: {0}")]
|
||||
CannotDecodeValuesInKey(frame_decode::storage::StorageKeyValueDecodeError),
|
||||
#[error(
|
||||
"Cannot decode storage key: there were leftover bytes, indicating that the decoding failed"
|
||||
)]
|
||||
LeftoverBytes { bytes: Vec<u8> },
|
||||
#[error("Can't decode a single value from the storage key part at index {index}: {error}")]
|
||||
CannotDecodeValueInKey {
|
||||
index: usize,
|
||||
error: scale_decode::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl StorageKeyError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum StorageValueError {
|
||||
#[error("Cannot decode storage value: {0}")]
|
||||
CannotDecode(frame_decode::storage::StorageValueDecodeError<u32>),
|
||||
#[error(
|
||||
"Cannot decode storage value: there were leftover bytes, indicating that the decoding failed"
|
||||
)]
|
||||
LeftoverBytes { bytes: Vec<u8> },
|
||||
}
|
||||
|
||||
impl StorageValueError {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
#[error("Cannot decode extrinsic at index {extrinsic_index}: {error}")]
|
||||
pub struct ExtrinsicDecodeErrorAt {
|
||||
pub extrinsic_index: usize,
|
||||
pub error: ExtrinsicDecodeErrorAtReason,
|
||||
}
|
||||
|
||||
impl ExtrinsicDecodeErrorAt {
|
||||
fn backend_error(&self) -> Option<&BackendError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ExtrinsicDecodeErrorAtReason {
|
||||
#[error("{0}")]
|
||||
DecodeError(frame_decode::extrinsics::ExtrinsicDecodeError),
|
||||
#[error("Leftover bytes")]
|
||||
LeftoverBytes(Vec<u8>),
|
||||
}
|
||||
|
||||
/// An error that can be emitted when trying to construct an instance of [`crate::config::ExtrinsicParams`],
|
||||
/// encode data from the instance, or match on signed extensions.
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ExtrinsicParamsError {
|
||||
#[error("Cannot find type id '{type_id} in the metadata (context: {context})")]
|
||||
MissingTypeId {
|
||||
/// Type ID.
|
||||
type_id: u32,
|
||||
/// Some arbitrary context to help narrow the source of the error.
|
||||
context: &'static str,
|
||||
},
|
||||
#[error("The chain expects a signed extension with the name {0}, but we did not provide one")]
|
||||
UnknownTransactionExtension(String),
|
||||
#[error("Error constructing extrinsic parameters: {0}")]
|
||||
Custom(Box<dyn core::error::Error + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsError {
|
||||
/// Create a custom [`ExtrinsicParamsError`] from a string.
|
||||
pub fn custom<S: Into<String>>(error: S) -> Self {
|
||||
let error: String = error.into();
|
||||
let error: Box<dyn core::error::Error + Send + Sync + 'static> = Box::from(error);
|
||||
ExtrinsicParamsError::Custom(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<core::convert::Infallible> for ExtrinsicParamsError {
|
||||
fn from(value: core::convert::Infallible) -> Self {
|
||||
match value {}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
//! something fails in trying to submit/execute a transaction.
|
||||
|
||||
use super::{DispatchErrorDecodeError, ModuleErrorDecodeError, ModuleErrorDetailsError};
|
||||
use crate::metadata::Metadata;
|
||||
use crate::metadata::ArcMetadata;
|
||||
use core::fmt::Debug;
|
||||
use scale_decode::{DecodeAsType, TypeResolver, visitor::DecodeAsTypeResult};
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
@@ -133,7 +133,7 @@ pub enum TransactionalError {
|
||||
#[derive(Clone, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub struct ModuleError {
|
||||
metadata: Metadata,
|
||||
metadata: ArcMetadata,
|
||||
/// Bytes representation:
|
||||
/// - `bytes[0]`: pallet index
|
||||
/// - `bytes[1]`: error index
|
||||
@@ -242,7 +242,7 @@ impl DispatchError {
|
||||
#[doc(hidden)]
|
||||
pub fn decode_from<'a>(
|
||||
bytes: impl Into<Cow<'a, [u8]>>,
|
||||
metadata: Metadata,
|
||||
metadata: ArcMetadata,
|
||||
) -> Result<Self, DispatchErrorDecodeError> {
|
||||
let bytes = bytes.into();
|
||||
let dispatch_error_ty_id = metadata
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
mod decode_as_event;
|
||||
|
||||
use crate::backend::BackendExt;
|
||||
use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT};
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::error::EventsError;
|
||||
use crate::{ArcMetadata, Metadata};
|
||||
use codec::{Compact, Decode, Encode};
|
||||
use scale_decode::{DecodeAsFields, DecodeAsType};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use decode_as_event::DecodeAsEvent;
|
||||
|
||||
/// A client for working with events.
|
||||
pub struct EventsClient<'atblock, T, Client> {
|
||||
client: &'atblock Client,
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'atblock, T, Client> EventsClient<'atblock, T, Client> {
|
||||
pub(crate) fn new(client: &'atblock Client) -> Self {
|
||||
EventsClient {
|
||||
client,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'atblock, T: Config, Client: OfflineClientAtBlockT<T>> EventsClient<'atblock, T, Client> {
|
||||
/// Work with the block event bytes given.
|
||||
///
|
||||
/// No attempt to validate the provided bytes is made here; if invalid bytes are
|
||||
/// provided then attempting to iterate and decode them will fail.
|
||||
pub fn from_bytes(&self, event_bytes: Vec<u8>) -> Events<T> {
|
||||
// event_bytes is a SCALE encoded vector of events. So, pluck the
|
||||
// compact encoded length from the front, leaving the remaining bytes
|
||||
// for our iterating to decode.
|
||||
//
|
||||
// Note: if we get no bytes back, avoid an error reading vec length
|
||||
// and default to 0 events.
|
||||
let cursor = &mut &*event_bytes;
|
||||
let num_events = <Compact<u32>>::decode(cursor).unwrap_or(Compact(0)).0;
|
||||
|
||||
// Start decoding after the compact encoded bytes.
|
||||
let start_idx = event_bytes.len() - cursor.len();
|
||||
|
||||
Events {
|
||||
metadata: self.client.metadata(),
|
||||
event_bytes: event_bytes.into(),
|
||||
start_idx,
|
||||
num_events,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'atblock, T: Config, Client: OnlineClientAtBlockT<T>> EventsClient<'atblock, T, Client> {
|
||||
/// Fetch the events at this block.
|
||||
pub async fn fetch(&self) -> Result<Events<T>, EventsError> {
|
||||
let client = self.client;
|
||||
|
||||
// Fetch the bytes. Ensure things work if we get 0 bytes back.
|
||||
let block_hash = client.block_hash();
|
||||
let event_bytes = client
|
||||
.backend()
|
||||
.storage_fetch_value(system_events_key().to_vec(), block_hash)
|
||||
.await
|
||||
.map_err(EventsError::CannotFetchEventBytes)?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(self.from_bytes(event_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// The events at some block.
|
||||
// Dev note [jsdw]:
|
||||
// It would be nice if this borrowed &'atblock Metadata, to be
|
||||
// consistent with many other things and allow longer lifetimes
|
||||
// on a couple of bits, but we need to construct this from transaction
|
||||
// things and can't provide lifetimes from there.
|
||||
#[derive(Debug)]
|
||||
pub struct Events<T> {
|
||||
metadata: ArcMetadata,
|
||||
// Note; raw event bytes are prefixed with a Compact<u32> containing
|
||||
// the number of events to be decoded. The start_idx reflects that, so
|
||||
// that we can skip over those bytes when decoding them
|
||||
event_bytes: Arc<[u8]>,
|
||||
start_idx: usize,
|
||||
num_events: u32,
|
||||
marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Events<T> {
|
||||
/// The number of events.
|
||||
pub fn len(&self) -> u32 {
|
||||
self.num_events
|
||||
}
|
||||
|
||||
/// Are there no events in this block?
|
||||
// Note: mainly here to satisfy clippy.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.num_events == 0
|
||||
}
|
||||
|
||||
/// Return the bytes representing all of the events.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.event_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<Event<'_, T>, EventsError>> + Send + Sync {
|
||||
// The event bytes ignoring the compact encoded length on the front:
|
||||
let event_bytes = self.event_bytes.clone();
|
||||
let metadata = &*self.metadata;
|
||||
let num_events = self.num_events;
|
||||
|
||||
let mut pos = self.start_idx;
|
||||
let mut index = 0;
|
||||
core::iter::from_fn(move || {
|
||||
if event_bytes.len() <= pos || num_events == index {
|
||||
None
|
||||
} else {
|
||||
match Event::decode_from(metadata, event_bytes.clone(), pos, index) {
|
||||
Ok(event_details) => {
|
||||
// Skip over decoded bytes in next iteration:
|
||||
pos += event_details.bytes().len();
|
||||
// Increment the index:
|
||||
index += 1;
|
||||
// Return the event details:
|
||||
Some(Ok(event_details))
|
||||
}
|
||||
Err(e) => {
|
||||
// By setting the position to the "end" of the event bytes,
|
||||
// the cursor len will become 0 and the iterator will return `None`
|
||||
// from now on:
|
||||
pos = event_bytes.len();
|
||||
Some(Err(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate through the events, Decoding and returning any that match the given type.
|
||||
///
|
||||
/// This is a convenience function for calling [`Self::iter`] and then [`Event::decode_call_data_fields_as`]
|
||||
/// on each event that we iterate over, filtering those that don't match.
|
||||
pub fn find<E: DecodeAsEvent>(&self) -> impl Iterator<Item = Result<E, EventsError>> {
|
||||
self.iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.decode_fields_as::<E>())
|
||||
}
|
||||
|
||||
/// Find the first event matching the given type, returning `None` if it doesn't exist,
|
||||
/// and the result of decoding it if it does.
|
||||
pub fn find_first<E: DecodeAsEvent>(&self) -> Option<Result<E, EventsError>> {
|
||||
self.find::<E>().next()
|
||||
}
|
||||
|
||||
/// Find an event matching the given type, returning true if it exists. This function does _not_
|
||||
/// try to actually decode the event bytes into the given type.
|
||||
pub fn has<E: DecodeAsEvent>(&self) -> bool {
|
||||
self.iter().filter_map(|e| e.ok()).any(|e| e.is::<E>())
|
||||
}
|
||||
}
|
||||
|
||||
/// A phase of a block's execution.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Decode, Encode)]
|
||||
pub enum Phase {
|
||||
/// Applying an extrinsic.
|
||||
ApplyExtrinsic(u32),
|
||||
/// Finalizing the block.
|
||||
Finalization,
|
||||
/// Initializing the block.
|
||||
Initialization,
|
||||
}
|
||||
|
||||
/// The event details.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Event<'events, T: Config> {
|
||||
pallet_name: &'events str,
|
||||
event_name: &'events str,
|
||||
metadata: &'events Metadata,
|
||||
// all of the event bytes (not just this one).
|
||||
all_bytes: Arc<[u8]>,
|
||||
// event phase.
|
||||
phase: Phase,
|
||||
/// The index of the event in the list of events in a given block.
|
||||
index: u32,
|
||||
// start of the bytes (phase, pallet/variant index and then fields and then topic to follow).
|
||||
start_idx: usize,
|
||||
// start of the event (ie pallet/variant index and then the fields and topic after).
|
||||
event_start_idx: usize,
|
||||
// start of the fields (ie after phase and pallet/variant index).
|
||||
event_fields_start_idx: usize,
|
||||
// end of the fields.
|
||||
event_fields_end_idx: usize,
|
||||
// end of everything (fields + topics)
|
||||
end_idx: usize,
|
||||
// event topics.
|
||||
topics: Vec<HashFor<T>>,
|
||||
}
|
||||
|
||||
impl<'events, T: Config> Event<'events, T> {
|
||||
/// Attempt to dynamically decode a single event from our events input.
|
||||
fn decode_from(
|
||||
metadata: &'events Metadata,
|
||||
all_bytes: Arc<[u8]>,
|
||||
start_idx: usize,
|
||||
index: u32,
|
||||
) -> Result<Event<'events, T>, EventsError> {
|
||||
let input = &mut &all_bytes[start_idx..];
|
||||
|
||||
let phase = Phase::decode(input).map_err(EventsError::CannotDecodePhase)?;
|
||||
|
||||
let event_start_idx = all_bytes.len() - input.len();
|
||||
|
||||
let pallet_index = u8::decode(input).map_err(EventsError::CannotDecodePalletIndex)?;
|
||||
let variant_index = u8::decode(input).map_err(EventsError::CannotDecodeVariantIndex)?;
|
||||
|
||||
let event_fields_start_idx = all_bytes.len() - input.len();
|
||||
|
||||
// Get metadata for the event:
|
||||
let event_pallet = metadata
|
||||
.pallet_by_event_index(pallet_index)
|
||||
.ok_or_else(|| EventsError::CannotFindPalletWithIndex(pallet_index))?;
|
||||
let event_variant = event_pallet
|
||||
.event_variant_by_index(variant_index)
|
||||
.ok_or_else(|| EventsError::CannotFindVariantWithIndex {
|
||||
pallet_name: event_pallet.name().to_string(),
|
||||
variant_index,
|
||||
})?;
|
||||
|
||||
tracing::debug!(
|
||||
"Decoding Event '{}::{}'",
|
||||
event_pallet.name(),
|
||||
&event_variant.name
|
||||
);
|
||||
|
||||
// Skip over the bytes belonging to this event.
|
||||
for field_metadata in &event_variant.fields {
|
||||
// Skip over the bytes for this field:
|
||||
scale_decode::visitor::decode_with_visitor(
|
||||
input,
|
||||
field_metadata.ty.id,
|
||||
metadata.types(),
|
||||
scale_decode::visitor::IgnoreVisitor::new(),
|
||||
)
|
||||
.map_err(|e| EventsError::CannotDecodeFieldInEvent {
|
||||
pallet_name: event_pallet.name().to_string(),
|
||||
event_name: event_variant.name.clone(),
|
||||
field_name: field_metadata
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or("<unknown>".to_string()),
|
||||
reason: e,
|
||||
})?;
|
||||
}
|
||||
|
||||
// the end of the field bytes.
|
||||
let event_fields_end_idx = all_bytes.len() - input.len();
|
||||
|
||||
// topics come after the event data in EventRecord.
|
||||
let topics =
|
||||
Vec::<HashFor<T>>::decode(input).map_err(EventsError::CannotDecodeEventTopics)?;
|
||||
|
||||
// what bytes did we skip over in total, including topics.
|
||||
let end_idx = all_bytes.len() - input.len();
|
||||
|
||||
Ok(Event {
|
||||
pallet_name: event_pallet.name(),
|
||||
event_name: &event_variant.name,
|
||||
phase,
|
||||
index,
|
||||
start_idx,
|
||||
event_start_idx,
|
||||
event_fields_start_idx,
|
||||
event_fields_end_idx,
|
||||
end_idx,
|
||||
all_bytes,
|
||||
metadata,
|
||||
topics,
|
||||
})
|
||||
}
|
||||
|
||||
/// When was the event produced?
|
||||
pub fn phase(&self) -> Phase {
|
||||
self.phase
|
||||
}
|
||||
|
||||
/// What index is this event in the stored events for this block.
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// The index of the pallet that the event originated from.
|
||||
pub fn pallet_index(&self) -> u8 {
|
||||
// Note: never panics; we expect these bytes to exist
|
||||
// in order that the EventDetails could be created.
|
||||
self.all_bytes[self.event_fields_start_idx - 2]
|
||||
}
|
||||
|
||||
/// The index of the event variant that the event originated from.
|
||||
pub fn event_index(&self) -> u8 {
|
||||
// Note: never panics; we expect these bytes to exist
|
||||
// in order that the EventDetails could be created.
|
||||
self.all_bytes[self.event_fields_start_idx - 1]
|
||||
}
|
||||
|
||||
/// The name of the pallet from whence the Event originated.
|
||||
pub fn pallet_name(&self) -> &'events str {
|
||||
self.pallet_name
|
||||
}
|
||||
|
||||
/// The name of the event (ie the name of the variant that it corresponds to).
|
||||
pub fn event_name(&self) -> &'events str {
|
||||
self.event_name
|
||||
}
|
||||
|
||||
/// 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.all_bytes[self.start_idx..self.end_idx]
|
||||
}
|
||||
|
||||
/// Return the bytes representing the fields stored in this event.
|
||||
pub fn field_bytes(&self) -> &[u8] {
|
||||
&self.all_bytes[self.event_fields_start_idx..self.event_fields_end_idx]
|
||||
}
|
||||
|
||||
/// Return the topics associated with this event.
|
||||
pub fn topics(&self) -> &[HashFor<T>] {
|
||||
&self.topics
|
||||
}
|
||||
|
||||
/// Return true if this [`Event`] matches the provided type.
|
||||
pub fn is<E: DecodeAsEvent>(&self) -> bool {
|
||||
E::is_event(self.pallet_name(), self.event_name())
|
||||
}
|
||||
|
||||
/// Attempt to decode this [`Event`] into an outer event enum type (which includes
|
||||
/// the pallet and event enum variants as well as the event fields). One compatible
|
||||
/// type for this is exposed via static codegen as a root level `Event` type.
|
||||
pub fn decode_as<E: DecodeAsType>(&self) -> Result<E, EventsError> {
|
||||
let bytes = &self.all_bytes[self.event_start_idx..self.event_fields_end_idx];
|
||||
|
||||
let decoded = E::decode_as_type(
|
||||
&mut &bytes[..],
|
||||
self.metadata.outer_enums().event_enum_ty(),
|
||||
self.metadata.types(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
let md = self.event_metadata();
|
||||
EventsError::CannotDecodeEventEnum {
|
||||
pallet_name: md.pallet.name().to_string(),
|
||||
event_name: md.variant.name.clone(),
|
||||
reason: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
/// Decode the event call data fields into some type which implements [`DecodeAsEvent`].
|
||||
///
|
||||
/// Event types generated via the [`crate::subxt!`] macro implement this.
|
||||
pub fn decode_fields_as<E: DecodeAsEvent>(&self) -> Option<Result<E, EventsError>> {
|
||||
if self.is::<E>() {
|
||||
Some(self.decode_fields_unchecked_as::<E>())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the event call data fields into some type which implements [`DecodeAsFields`].
|
||||
///
|
||||
/// This ignores the pallet and event name information, so you should check those via [`Self::pallet_name()`]
|
||||
/// and [`Self::event_name()`] to confirm that this event is the one you are intending to decode.
|
||||
///
|
||||
/// Prefer to use [`Self::decode_call_data_fields_as`] where possible.
|
||||
pub fn decode_fields_unchecked_as<E: DecodeAsFields>(&self) -> Result<E, EventsError> {
|
||||
let bytes = &mut self.field_bytes();
|
||||
let event_metadata = self.event_metadata();
|
||||
|
||||
let mut fields = event_metadata
|
||||
.variant
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| scale_decode::Field::new(f.ty.id, f.name.as_deref()));
|
||||
|
||||
let decoded =
|
||||
E::decode_as_fields(bytes, &mut fields, self.metadata.types()).map_err(|e| {
|
||||
EventsError::CannotDecodeEventFields {
|
||||
pallet_name: event_metadata.pallet.name().to_string(),
|
||||
event_name: event_metadata.variant.name.clone(),
|
||||
reason: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
/// Fetch details from the metadata for this event. This is used for decoding but
|
||||
/// we try to avoid using it elsewhere.
|
||||
fn event_metadata(&self) -> EventMetadataDetails<'_> {
|
||||
let pallet = self
|
||||
.metadata
|
||||
.pallet_by_event_index(self.pallet_index())
|
||||
.expect("event pallet to be found; we did this already during decoding");
|
||||
let variant = pallet
|
||||
.event_variant_by_index(self.event_index())
|
||||
.expect("event variant to be found; we did this already during decoding");
|
||||
|
||||
EventMetadataDetails { pallet, variant }
|
||||
}
|
||||
}
|
||||
|
||||
// The storage key needed to access events.
|
||||
fn system_events_key() -> [u8; 32] {
|
||||
let a = sp_crypto_hashing::twox_128(b"System");
|
||||
let b = sp_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
|
||||
}
|
||||
|
||||
/// Details for the given event plucked from the metadata.
|
||||
struct EventMetadataDetails<'a> {
|
||||
/// Metadata for the pallet that the event belongs to.
|
||||
pub pallet: subxt_metadata::PalletMetadata<'a>,
|
||||
/// Metadata for the variant which describes the pallet events.
|
||||
pub variant: &'a scale_info::Variant<scale_info::form::PortableForm>,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use scale_decode::DecodeAsFields;
|
||||
|
||||
/// This trait can be implemented for any type which implements [`DecodeAsFields`].
|
||||
/// This adds information to the type about which event it is, which enforces that
|
||||
/// only the correct event can be decoded into it.
|
||||
pub trait DecodeAsEvent: DecodeAsFields {
|
||||
/// Pallet name.
|
||||
const PALLET_NAME: &'static str;
|
||||
/// Event name.
|
||||
const EVENT_NAME: &'static str;
|
||||
|
||||
/// Returns true if the given pallet and event names match this event.
|
||||
fn is_event(pallet: &str, event: &str) -> bool {
|
||||
Self::PALLET_NAME == pallet && Self::EVENT_NAME == event
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
// 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 = sp_crypto_hashing::twox_128(b"System");
|
||||
let b = sp_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)
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
use crate::{
|
||||
Metadata,
|
||||
config::{Config, HashFor},
|
||||
error::EventsError,
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
use scale_decode::{DecodeAsFields, DecodeAsType};
|
||||
use subxt_core::events::{EventDetails as CoreEventDetails, Events as CoreEvents};
|
||||
|
||||
pub use 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 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 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()
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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 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))
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
mod decode_as_extrinsic;
|
||||
mod extrinsic_transaction_extensions;
|
||||
|
||||
use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT};
|
||||
use crate::config::{Config, HashFor, Hasher};
|
||||
use crate::error::{
|
||||
EventsError, ExtrinsicDecodeErrorAt, ExtrinsicDecodeErrorAtReason, ExtrinsicError,
|
||||
};
|
||||
use crate::events::{self, DecodeAsEvent};
|
||||
use frame_decode::extrinsics::Extrinsic as ExtrinsicInfo;
|
||||
use scale_decode::{DecodeAsFields, DecodeAsType};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use subxt_metadata::Metadata;
|
||||
|
||||
pub use decode_as_extrinsic::DecodeAsExtrinsic;
|
||||
pub use extrinsic_transaction_extensions::{
|
||||
ExtrinsicTransactionExtension, ExtrinsicTransactionExtensions,
|
||||
};
|
||||
|
||||
/// A client for working with extrinsics.
|
||||
pub struct ExtrinsicsClient<'atblock, T, Client> {
|
||||
client: &'atblock Client,
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'atblock, T, Client> ExtrinsicsClient<'atblock, T, Client> {
|
||||
pub(crate) fn new(client: &'atblock Client) -> Self {
|
||||
ExtrinsicsClient {
|
||||
client,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'atblock, T: Config, Client: OfflineClientAtBlockT<T>> ExtrinsicsClient<'atblock, T, Client> {
|
||||
/// Work with the block body bytes given.
|
||||
///
|
||||
/// No attempt to validate the provided bytes is made here; if invalid bytes are
|
||||
/// provided then attempting to iterate and decode them will fail.
|
||||
pub async fn from_bytes(&self, extrinsics: Vec<Vec<u8>>) -> Extrinsics<'atblock, T, Client> {
|
||||
Extrinsics {
|
||||
client: self.client,
|
||||
extrinsics: Arc::new(extrinsics),
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'atblock, T: Config, Client: OnlineClientAtBlockT<T>> ExtrinsicsClient<'atblock, T, Client> {
|
||||
/// Fetch the extrinsics at this block.
|
||||
pub async fn fetch(&self) -> Result<Extrinsics<'atblock, T, Client>, ExtrinsicError> {
|
||||
let client = self.client;
|
||||
let block_hash = client.block_hash();
|
||||
let extrinsics = client
|
||||
.backend()
|
||||
.block_body(block_hash)
|
||||
.await
|
||||
.map_err(ExtrinsicError::CannotGetBlockBody)?
|
||||
.ok_or_else(|| ExtrinsicError::BlockNotFound(block_hash.into()))?;
|
||||
|
||||
Ok(Extrinsics {
|
||||
client,
|
||||
extrinsics: Arc::new(extrinsics),
|
||||
marker: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The extrinsics in a block.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Extrinsics<'atblock, T, C> {
|
||||
client: &'atblock C,
|
||||
extrinsics: Arc<Vec<Vec<u8>>>,
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'atblock, T: Config, C: OfflineClientAtBlockT<T>> Extrinsics<'atblock, T, C> {
|
||||
/// The number of extrinsics.
|
||||
pub fn len(&self) -> usize {
|
||||
self.extrinsics.len()
|
||||
}
|
||||
|
||||
/// Are there no extrinsics in this block?
|
||||
// Note: mainly here to satisfy clippy.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.extrinsics.is_empty()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the extrinsics in the block body. We decode the extrinsics on
|
||||
/// demand as we iterate, and so if any fail to decode an error will be returned.
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<Extrinsic<'_, T, C>, ExtrinsicDecodeErrorAt>> {
|
||||
let hasher = self.client.hasher();
|
||||
let metadata = self.client.metadata_ref();
|
||||
let client = self.client;
|
||||
let all_extrinsic_bytes = self.extrinsics.clone();
|
||||
|
||||
self.extrinsics
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(extrinsic_index, extrinsic_bytes)| {
|
||||
let cursor = &mut &**extrinsic_bytes;
|
||||
|
||||
// Try to decode the extrinsic.
|
||||
let info =
|
||||
frame_decode::extrinsics::decode_extrinsic(cursor, metadata, metadata.types())
|
||||
.map_err(|error| ExtrinsicDecodeErrorAt {
|
||||
extrinsic_index,
|
||||
error: ExtrinsicDecodeErrorAtReason::DecodeError(error),
|
||||
})?
|
||||
.into_owned();
|
||||
|
||||
// We didn't consume all bytes, so decoding probably failed.
|
||||
if !cursor.is_empty() {
|
||||
return Err(ExtrinsicDecodeErrorAt {
|
||||
extrinsic_index,
|
||||
error: ExtrinsicDecodeErrorAtReason::LeftoverBytes(cursor.to_vec()),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Extrinsic {
|
||||
client: client,
|
||||
index: extrinsic_index,
|
||||
info: Arc::new(info),
|
||||
extrinsics: Arc::clone(&all_extrinsic_bytes),
|
||||
hasher,
|
||||
metadata,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate through the extrinsics, Decoding and returning any that match the given type.
|
||||
///
|
||||
/// This is a convenience function for calling [`Self::iter`] and then [`Extrinsic::decode_call_data_fields_as`]
|
||||
/// on each extrinsic that we iterate over, filtering those that don't match.
|
||||
pub fn find<E: DecodeAsExtrinsic>(&self) -> impl Iterator<Item = Result<E, ExtrinsicError>> {
|
||||
self.iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.decode_call_data_fields_as::<E>())
|
||||
}
|
||||
|
||||
/// Find the first extrinsic matching the given type, returning `None` if it doesn't exist,
|
||||
/// and the result of decoding it if it does.
|
||||
pub fn find_first<E: DecodeAsExtrinsic>(&self) -> Option<Result<E, ExtrinsicError>> {
|
||||
self.find::<E>().next()
|
||||
}
|
||||
|
||||
/// Find an extrinsic matching the given type, returning true if it exists. This function does _not_
|
||||
/// try to actually decode the extrinsic bytes into the given type.
|
||||
pub fn has<E: DecodeAsExtrinsic>(&self) -> bool {
|
||||
self.iter().filter_map(|e| e.ok()).any(|e| e.is::<E>())
|
||||
}
|
||||
}
|
||||
|
||||
/// A single extrinsic in a block.
|
||||
pub struct Extrinsic<'atblock, T: Config, C> {
|
||||
client: &'atblock C,
|
||||
/// The index of the extrinsic in the block.
|
||||
index: usize,
|
||||
/// Information about the extrinsic
|
||||
info: Arc<ExtrinsicInfo<'atblock, u32>>,
|
||||
/// All extrinsic bytes. use the index to select the correct bytes.
|
||||
extrinsics: Arc<Vec<Vec<u8>>>,
|
||||
/// Hash the extrinsic if we want.
|
||||
hasher: &'atblock T::Hasher,
|
||||
/// Subxt metadata to fetch the extrinsic metadata.
|
||||
metadata: &'atblock Metadata,
|
||||
}
|
||||
|
||||
impl<'atblock, T, C> Extrinsic<'atblock, T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OfflineClientAtBlockT<T>,
|
||||
{
|
||||
/// Calculate and return the hash of the extrinsic, based on the configured hasher.
|
||||
pub fn hash(&self) -> HashFor<T> {
|
||||
self.hasher.hash(&self.extrinsics[self.index])
|
||||
}
|
||||
|
||||
/// Is the extrinsic signed?
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.info.is_signed()
|
||||
}
|
||||
|
||||
/// The index of the extrinsic in the block.
|
||||
pub fn index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// The index of the pallet that the extrinsic originated from.
|
||||
pub fn pallet_index(&self) -> u8 {
|
||||
self.info.pallet_index()
|
||||
}
|
||||
|
||||
/// The index of the extrinsic variant that the extrinsic originated from.
|
||||
pub fn call_index(&self) -> u8 {
|
||||
self.info.call_index()
|
||||
}
|
||||
|
||||
/// The name of the pallet from whence the extrinsic originated.
|
||||
pub fn pallet_name(&self) -> &str {
|
||||
self.info.pallet_name()
|
||||
}
|
||||
|
||||
/// The name of the call (ie the name of the variant that it corresponds to).
|
||||
pub fn call_name(&self) -> &str {
|
||||
self.info.call_name()
|
||||
}
|
||||
|
||||
/// Return the extrinsic bytes.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.extrinsics[self.index]
|
||||
}
|
||||
|
||||
/// Return only the bytes representing this extrinsic call:
|
||||
/// - First byte is the pallet index
|
||||
/// - Second byte is the variant (call) index
|
||||
/// - Followed by field bytes.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Please use [`Self::bytes`] if you want to get all extrinsic bytes.
|
||||
pub fn call_data_bytes(&self) -> &[u8] {
|
||||
&self.bytes()[self.info.call_data_range()]
|
||||
}
|
||||
|
||||
/// Return the bytes representing the fields stored in this extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a subset of [`Self::call_bytes`] that does not include the
|
||||
/// first two bytes that denote the pallet index and the variant index.
|
||||
pub fn call_data_field_bytes(&self) -> &[u8] {
|
||||
&self.bytes()[self.info.call_data_args_range()]
|
||||
}
|
||||
|
||||
/// Return only the bytes of the address that signed this extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Returns `None` if the extrinsic is not signed.
|
||||
pub fn address_bytes(&self) -> Option<&[u8]> {
|
||||
self.info
|
||||
.signature_payload()
|
||||
.map(|s| &self.bytes()[s.address_range()])
|
||||
}
|
||||
|
||||
/// Returns Some(signature_bytes) if the extrinsic was signed otherwise None is returned.
|
||||
pub fn signature_bytes(&self) -> Option<&[u8]> {
|
||||
self.info
|
||||
.signature_payload()
|
||||
.map(|s| &self.bytes()[s.signature_range()])
|
||||
}
|
||||
|
||||
/// Returns the signed extension `extra` bytes of the extrinsic.
|
||||
/// Each signed extension has an `extra` type (May be zero-sized).
|
||||
/// These bytes are the scale encoded `extra` fields of each signed extension in order of the signed extensions.
|
||||
/// They do *not* include the `additional` signed bytes that are used as part of the payload that is signed.
|
||||
///
|
||||
/// Note: Returns `None` if the extrinsic is not signed.
|
||||
pub fn transaction_extensions_bytes(&self) -> Option<&[u8]> {
|
||||
self.info
|
||||
.transaction_extension_payload()
|
||||
.map(|t| &self.bytes()[t.range()])
|
||||
}
|
||||
|
||||
/// Returns `None` if the extrinsic is not signed.
|
||||
pub fn transaction_extensions(
|
||||
&self,
|
||||
) -> Option<ExtrinsicTransactionExtensions<'atblock, '_, T>> {
|
||||
let bytes = self.bytes();
|
||||
let metadata = self.metadata;
|
||||
|
||||
self.info
|
||||
.transaction_extension_payload()
|
||||
.map(move |t| ExtrinsicTransactionExtensions::new(bytes, metadata, t))
|
||||
}
|
||||
|
||||
/// Return true if this [`Extrinsic`] matches the provided type.
|
||||
pub fn is<E: DecodeAsExtrinsic>(&self) -> bool {
|
||||
E::is_extrinsic(self.pallet_name(), self.call_name())
|
||||
}
|
||||
|
||||
/// Attempt to decode this [`Extrinsic`] into an outer call enum type (which includes
|
||||
/// the pallet and extrinsic enum variants as well as the extrinsic fields). One compatible
|
||||
/// type for this is exposed via static codegen as a root level `Call` type.
|
||||
pub fn decode_call_data_as<E: DecodeAsType>(&self) -> Result<E, ExtrinsicError> {
|
||||
let decoded = E::decode_as_type(
|
||||
&mut &self.call_data_bytes()[..],
|
||||
self.metadata.outer_enums().call_enum_ty(),
|
||||
self.metadata.types(),
|
||||
)
|
||||
.map_err(|e| ExtrinsicError::CannotDecodeIntoRootExtrinsic {
|
||||
extrinsic_index: self.index as usize,
|
||||
error: e,
|
||||
})?;
|
||||
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
/// Decode the extrinsic call data fields into some type which implements [`DecodeAsExtrinsic`].
|
||||
///
|
||||
/// Extrinsic types generated via the [`crate::subxt!`] macro implement this.
|
||||
pub fn decode_call_data_fields_as<E: DecodeAsExtrinsic>(
|
||||
&self,
|
||||
) -> Option<Result<E, ExtrinsicError>> {
|
||||
if self.is::<E>() {
|
||||
Some(self.decode_call_data_fields_unchecked_as::<E>())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the extrinsic call data fields into some type which implements [`DecodeAsFields`].
|
||||
///
|
||||
/// This ignores the pallet and call name information, so you should check those via [`Self::pallet_name()`]
|
||||
/// and [`Self::call_name()`] to confirm that this extrinsic is the one you are intending to decode.
|
||||
///
|
||||
/// Prefer to use [`Self::decode_call_data_fields_as`] where possible.
|
||||
pub fn decode_call_data_fields_unchecked_as<E: DecodeAsFields>(
|
||||
&self,
|
||||
) -> Result<E, ExtrinsicError> {
|
||||
let bytes = &mut self.call_data_field_bytes();
|
||||
let mut fields = self.info.call_data().map(|d| {
|
||||
let name = if d.name().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(d.name())
|
||||
};
|
||||
scale_decode::Field::new(*d.ty(), name)
|
||||
});
|
||||
let decoded =
|
||||
E::decode_as_fields(bytes, &mut fields, self.metadata.types()).map_err(|e| {
|
||||
ExtrinsicError::CannotDecodeFields {
|
||||
extrinsic_index: self.index as usize,
|
||||
error: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'atblock, T, C> Extrinsic<'atblock, T, C>
|
||||
where
|
||||
T: Config,
|
||||
C: OnlineClientAtBlockT<T>,
|
||||
{
|
||||
/// The events associated with the extrinsic.
|
||||
pub async fn events(&self) -> Result<ExtrinsicEvents<T>, EventsError> {
|
||||
ExtrinsicEvents::fetch(self.client, self.hash(), self.index()).await
|
||||
}
|
||||
}
|
||||
|
||||
/// The events associated with a given extrinsic.
|
||||
#[derive(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).
|
||||
extrinsic_hash: HashFor<T>,
|
||||
// The index of the extrinsic:
|
||||
extrinsic_index: usize,
|
||||
// All of the events in the block:
|
||||
events: crate::events::Events<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicEvents<T> {
|
||||
pub(crate) async fn fetch(
|
||||
client: &impl OnlineClientAtBlockT<T>,
|
||||
extrinsic_hash: HashFor<T>,
|
||||
extrinsic_index: usize,
|
||||
) -> Result<Self, EventsError> {
|
||||
let events = crate::events::EventsClient::new(client).fetch().await?;
|
||||
Ok(ExtrinsicEvents {
|
||||
extrinsic_hash,
|
||||
extrinsic_index,
|
||||
events,
|
||||
})
|
||||
}
|
||||
|
||||
/// The index of the extrinsic that these events are produced from.
|
||||
pub fn extrinsic_index(&self) -> usize {
|
||||
self.extrinsic_index
|
||||
}
|
||||
|
||||
/// Return the hash of the extrinsic.
|
||||
pub fn extrinsic_hash(&self) -> HashFor<T> {
|
||||
self.extrinsic_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 extrinsic.
|
||||
///
|
||||
/// This works in the same way that [`events::Events::iter()`] does, with the
|
||||
/// exception that it filters out events not related to the current extrinsic.
|
||||
pub fn iter(&'_ self) -> impl Iterator<Item = Result<events::Event<'_, T>, EventsError>> {
|
||||
self.events.iter().filter(|ev| {
|
||||
ev.as_ref()
|
||||
.map(|ev| ev.phase() == events::Phase::ApplyExtrinsic(self.extrinsic_index as u32))
|
||||
.unwrap_or(true) // Keep any errors.
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate through the extrinsic's events, Decoding and returning any that match the given type.
|
||||
///
|
||||
/// This is a convenience function for calling [`Self::iter`] and then [`events::Event::decode_fields_as`]
|
||||
/// on each event that we iterate over, filtering those that don't match.
|
||||
pub fn find<E: DecodeAsEvent>(&self) -> impl Iterator<Item = Result<E, EventsError>> {
|
||||
self.iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.decode_fields_as::<E>())
|
||||
}
|
||||
|
||||
/// Find the first event matching the given type, returning `None` if it doesn't exist,
|
||||
/// and the result of decoding it if it does.
|
||||
pub fn find_first<E: DecodeAsEvent>(&self) -> Option<Result<E, EventsError>> {
|
||||
self.find::<E>().next()
|
||||
}
|
||||
|
||||
/// Find an event matching the given type, returning true if it exists. This function does _not_
|
||||
/// try to actually decode the event bytes into the given type.
|
||||
pub fn has<E: DecodeAsEvent>(&self) -> bool {
|
||||
self.iter().filter_map(|e| e.ok()).any(|e| e.is::<E>())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use scale_decode::DecodeAsFields;
|
||||
|
||||
/// This trait can be implemented for any type which implements [`DecodeAsFields`].
|
||||
/// This adds information to the type about which extrinsic it is, which enforces that
|
||||
/// only the correct extrinsic can be decoded into it.
|
||||
pub trait DecodeAsExtrinsic: DecodeAsFields {
|
||||
/// Pallet name.
|
||||
const PALLET_NAME: &'static str;
|
||||
/// Call name.
|
||||
const CALL_NAME: &'static str;
|
||||
|
||||
/// Returns true if the given pallet and call names match this extrinsic.
|
||||
fn is_extrinsic(pallet: &str, call: &str) -> bool {
|
||||
Self::PALLET_NAME == pallet && Self::CALL_NAME == call
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
use crate::config::Config;
|
||||
use crate::config::TransactionExtension;
|
||||
use crate::config::transaction_extensions::{
|
||||
ChargeAssetTxPayment, ChargeTransactionPayment, CheckNonce,
|
||||
};
|
||||
use crate::error::ExtrinsicError;
|
||||
use frame_decode::extrinsics::ExtrinsicExtensions as ExtrinsicExtensionsInfo;
|
||||
use scale_decode::DecodeAsType;
|
||||
use subxt_metadata::Metadata;
|
||||
|
||||
/// The signed extensions of an extrinsic.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtrinsicTransactionExtensions<'atblock, 'extrinsic, T: Config> {
|
||||
bytes: &'extrinsic [u8],
|
||||
metadata: &'atblock Metadata,
|
||||
decoded_info: &'extrinsic ExtrinsicExtensionsInfo<'atblock, u32>,
|
||||
marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'atblock, 'extrinsic, T: Config> ExtrinsicTransactionExtensions<'atblock, 'extrinsic, T> {
|
||||
pub(crate) fn new(
|
||||
bytes: &'extrinsic [u8],
|
||||
metadata: &'atblock Metadata,
|
||||
decoded_info: &'extrinsic ExtrinsicExtensionsInfo<'atblock, u32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
bytes,
|
||||
metadata,
|
||||
decoded_info,
|
||||
marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over each of the signed extension details of the extrinsic.
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = ExtrinsicTransactionExtension<'atblock, 'extrinsic, T>> {
|
||||
self.decoded_info
|
||||
.iter()
|
||||
.map(move |s| ExtrinsicTransactionExtension {
|
||||
bytes: &self.bytes[s.range()],
|
||||
ty_id: *s.ty(),
|
||||
identifier: s.name(),
|
||||
metadata: self.metadata,
|
||||
_marker: core::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Searches through all signed extensions to find a specific one.
|
||||
/// If the Signed Extension is not found `Ok(None)` is returned.
|
||||
/// If the Signed Extension is found but decoding failed `Err(_)` is returned.
|
||||
pub fn find<S: TransactionExtension<T>>(&self) -> Option<Result<S::Decoded, ExtrinsicError>> {
|
||||
for ext in self.iter() {
|
||||
if let Some(e) = ext.decode_as::<S>() {
|
||||
return Some(e);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The tip of an extrinsic, extracted from the ChargeTransactionPayment or ChargeAssetTxPayment
|
||||
/// signed extension, depending on which is present.
|
||||
///
|
||||
/// Returns `None` if `tip` was not found or decoding failed.
|
||||
pub fn tip(&self) -> Option<u128> {
|
||||
// Note: the overhead of iterating multiple time should be negligible.
|
||||
if let Some(tip) = self.find::<ChargeTransactionPayment>() {
|
||||
return Some(tip.ok()?.tip());
|
||||
}
|
||||
if let Some(tip) = self.find::<ChargeAssetTxPayment<T>>() {
|
||||
return Some(tip.ok()?.tip());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The nonce of the account that submitted the extrinsic, extracted from the CheckNonce signed extension.
|
||||
///
|
||||
/// Returns `None` if `nonce` was not found or decoding failed.
|
||||
pub fn nonce(&self) -> Option<u64> {
|
||||
self.find::<CheckNonce>()?.ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// A single signed extension
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtrinsicTransactionExtension<'atblock, 'extrinsic, T: Config> {
|
||||
bytes: &'extrinsic [u8],
|
||||
ty_id: u32,
|
||||
identifier: &'extrinsic str,
|
||||
metadata: &'atblock Metadata,
|
||||
_marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'atblock, 'extrinsic, T: Config> ExtrinsicTransactionExtension<'atblock, 'extrinsic, T> {
|
||||
/// The bytes representing this signed extension.
|
||||
pub fn bytes(&self) -> &'extrinsic [u8] {
|
||||
self.bytes
|
||||
}
|
||||
|
||||
/// The name of the signed extension.
|
||||
pub fn name(&self) -> &'extrinsic str {
|
||||
self.identifier
|
||||
}
|
||||
|
||||
/// The type id of the signed extension.
|
||||
pub fn type_id(&self) -> u32 {
|
||||
self.ty_id
|
||||
}
|
||||
|
||||
/// Decodes this signed extension based on the provided [`TransactionExtension`] type.
|
||||
pub fn decode_as<S: TransactionExtension<T>>(
|
||||
&self,
|
||||
) -> Option<Result<S::Decoded, ExtrinsicError>> {
|
||||
if !S::matches(self.identifier, self.ty_id, self.metadata.types()) {
|
||||
return None;
|
||||
}
|
||||
Some(self.decode_unchecked_as::<S::Decoded>())
|
||||
}
|
||||
|
||||
/// Decode the extension into some type which implements [`DecodeAsType`].
|
||||
///
|
||||
/// This ignores the extension name, so you should first check that this is what you expect
|
||||
/// via [`Self::name()`].
|
||||
///
|
||||
/// Prefer to use [`Self::decode_as`] where possible.
|
||||
pub fn decode_unchecked_as<E: DecodeAsType>(&self) -> Result<E, ExtrinsicError> {
|
||||
let value = E::decode_as_type(&mut &self.bytes[..], self.ty_id, self.metadata.types())
|
||||
.map_err(|e| ExtrinsicError::CouldNotDecodeTransactionExtension {
|
||||
name: self.identifier.to_owned(),
|
||||
error: e,
|
||||
})?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
+34
-77
@@ -2,94 +2,71 @@
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
// TODO: REMOVE BEFORE MERGING.
|
||||
#![allow(missing_docs)]
|
||||
|
||||
//! 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")]
|
||||
#![doc = include_str!("../examples/transactions_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;
|
||||
// TODO: Do we need this still?
|
||||
// // Suppress an unused dependency warning because these are
|
||||
// // only used in example code snippets at the time of writing.
|
||||
// #[cfg(test)]
|
||||
// mod only_used_in_docs_or_tests {
|
||||
// use subxt_signer as _;
|
||||
// use tokio as _;
|
||||
// use tracing_subscriber as _;
|
||||
// }
|
||||
|
||||
// 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 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 _;
|
||||
// This is exposed so that the code generated by subxt-codegen can avoid
|
||||
// relying on std things. Given that it relies on subxt, it _must_ use std,
|
||||
// but this may change if we move things back to a no-std core/common crate.
|
||||
// that it can point at.
|
||||
//
|
||||
// Undocumented and **should not be depended on by anybody else**.
|
||||
#[doc(hidden)]
|
||||
pub extern crate alloc;
|
||||
|
||||
pub mod backend;
|
||||
pub mod blocks;
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
pub mod custom_values;
|
||||
pub mod dynamic;
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod runtime_api;
|
||||
pub mod extrinsics;
|
||||
pub mod metadata;
|
||||
pub mod runtime_apis;
|
||||
pub mod storage;
|
||||
pub mod tx;
|
||||
pub mod transactions;
|
||||
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 subxt_core::config::{
|
||||
Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, ExtrinsicParams,
|
||||
ExtrinsicParamsEncoder, Hash, HashFor, Hasher, Header, PolkadotConfig,
|
||||
PolkadotExtrinsicParams, SubstrateConfig, SubstrateExtrinsicParams, TransactionExtension,
|
||||
polkadot, substrate, transaction_extensions,
|
||||
};
|
||||
pub use subxt_core::error::ExtrinsicParamsError;
|
||||
}
|
||||
|
||||
/// Types representing the metadata obtained from a node.
|
||||
pub mod metadata {
|
||||
pub use subxt_metadata::*;
|
||||
}
|
||||
|
||||
/// Submit dynamic transactions.
|
||||
pub mod dynamic {
|
||||
pub use subxt_core::dynamic::*;
|
||||
}
|
||||
|
||||
// Expose light client bits
|
||||
cfg_unstable_light_client! {
|
||||
pub use 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,
|
||||
metadata::{ArcMetadata, Metadata},
|
||||
};
|
||||
|
||||
// Expose light client bits
|
||||
#[cfg(feature = "unstable-light-client")]
|
||||
pub use subxt_lightclient as lightclient;
|
||||
|
||||
/// Re-export external crates that are made use of in the subxt API.
|
||||
pub mod ext {
|
||||
pub use codec;
|
||||
@@ -99,12 +76,10 @@ pub mod ext {
|
||||
pub use scale_decode;
|
||||
pub use scale_encode;
|
||||
pub use scale_value;
|
||||
pub use subxt_core;
|
||||
pub use subxt_rpcs;
|
||||
|
||||
cfg_jsonrpsee! {
|
||||
pub use jsonrpsee;
|
||||
}
|
||||
#[cfg(feature = "jsonrpsee")]
|
||||
pub use jsonrpsee;
|
||||
}
|
||||
|
||||
/// Generate a strongly typed API for interacting with a Substrate runtime from its metadata of WASM.
|
||||
@@ -146,15 +121,6 @@ pub mod ext {
|
||||
/// 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.
|
||||
@@ -168,15 +134,6 @@ pub mod ext {
|
||||
/// 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,
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// 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,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
// Re-export everything from subxt-metadata here.
|
||||
pub use subxt_metadata::*;
|
||||
|
||||
/// A cheaply clonable version of our [`Metadata`].
|
||||
pub type ArcMetadata = Arc<Metadata>;
|
||||
@@ -1,12 +0,0 @@
|
||||
// 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 subxt_core::runtime_api::payload::{DynamicPayload, Payload, StaticPayload, dynamic};
|
||||
@@ -1,61 +0,0 @@
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// 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> {
|
||||
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.
|
||||
subxt_core::runtime_api::validate(&payload, &metadata)?;
|
||||
|
||||
// Encode the arguments of the runtime call.
|
||||
let call_name = subxt_core::runtime_api::call_name(&payload);
|
||||
let call_args = 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 = subxt_core::runtime_api::decode_value(&mut &*bytes, &payload, &metadata)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user