fix: Convert vendor/pezkuwi-subxt from submodule to regular directory

This commit is contained in:
2025-12-19 16:45:24 +03:00
parent 9a52edf0df
commit fdd023c499
393 changed files with 154124 additions and 1 deletions
@@ -0,0 +1,55 @@
[package]
name = "integration-tests"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = false
license.workspace = true
repository.workspace = true
documentation.workspace = true
homepage.workspace = true
description = "Subxt integration tests that rely on the Substrate binary"
[features]
default = []
# Enable to run the tests with Light Client support.
unstable-light-client = ["subxt/unstable-light-client"]
# Enable to run the full-client tests with Light Client support.
unstable-light-client-long-running = ["subxt/unstable-light-client"]
# Enable this to use the chainhead backend in tests _instead of_
# the default one which relies on the "old" RPC methods.
chainhead-backend = []
[dev-dependencies]
assert_matches = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true, features = ["derive", "bit-vec"] }
frame-decode = { workspace = true }
frame-metadata = { workspace = true }
futures = { workspace = true }
hex = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
scale-info = { workspace = true, features = ["bit-vec"] }
scale-value = { workspace = true }
pezsp-core = { workspace = true, features = ["std"] }
syn = { workspace = true }
pezkuwi-subxt = { workspace = true, features = ["unstable-metadata", "native", "jsonrpsee", "reconnecting-rpc-client"] }
pezkuwi-subxt-signer = { workspace = true, features = ["default"] }
pezkuwi-subxt-codegen = { workspace = true }
pezkuwi-subxt-metadata = { workspace = true }
pezkuwi-subxt-rpcs = { workspace = true }
test-runtime = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
wat = { workspace = true }
substrate-runner = { workspace = true }
subxt-test-macro = { path = "subxt-test-macro" }
[build-dependencies]
cfg_aliases = "0.2.1"
+11
View File
@@ -0,0 +1,11 @@
use cfg_aliases::cfg_aliases;
fn main() {
// Setup cfg aliases
cfg_aliases! {
lightclient: { any(feature = "unstable-light-client", feature = "unstable-light-client-long-running") },
fullclient: { all(not(feature = "unstable-light-client"), not(feature = "unstable-light-client-long-running")) },
legacy_backend: { not(feature = "chainhead-backend") },
chainhead_backend: { feature = "chainhead-backend" },
}
}
@@ -0,0 +1,444 @@
// 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::{subxt_test, test_context, utils::consume_initial_blocks};
use codec::{Compact, Decode, Encode};
use futures::StreamExt;
#[cfg(fullclient)]
use crate::utils::node_runtime;
#[cfg(fullclient)]
use subxt::{
config::{
DefaultExtrinsicParamsBuilder, SubstrateConfig,
transaction_extensions::{ChargeAssetTxPayment, CheckMortality, CheckNonce},
},
utils::Era,
};
#[cfg(fullclient)]
use pezkuwi_subxt_signer::sr25519::dev;
#[cfg(fullclient)]
#[subxt_test]
async fn block_subscriptions_are_consistent_with_eachother() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let mut all_sub = api.blocks().subscribe_all().await?;
let mut best_sub = api.blocks().subscribe_best().await?;
let mut finalized_sub = api.blocks().subscribe_finalized().await?;
let mut finals = vec![];
let mut bests = vec![];
let mut alls = vec![];
// Finalization can run behind a bit; blocks that were reported a while ago can
// only just now be being finalized (in the new RPCs this isn't true and we'll be
// told about all of those blocks up front). So, first we wait until finalization reports
// a block that we've seen as new.
loop {
tokio::select! {biased;
Some(Ok(b)) = all_sub.next() => alls.push(b.hash()),
Some(Ok(b)) = best_sub.next() => bests.push(b.hash()),
Some(Ok(b)) = finalized_sub.next() => if alls.contains(&b.hash()) { break },
}
}
// Now, gather a couple more finalized blocks as well as anything else we hear about.
while finals.len() < 2 {
tokio::select! {biased;
Some(Ok(b)) = all_sub.next() => alls.push(b.hash()),
Some(Ok(b)) = best_sub.next() => bests.push(b.hash()),
Some(Ok(b)) = finalized_sub.next() => finals.push(b.hash()),
}
}
// Check that the items in the first slice are found in the same order in the second slice.
fn are_same_order_in<T: PartialEq>(a_items: &[T], b_items: &[T]) -> bool {
let mut b_idx = 0;
for a in a_items {
if let Some((idx, _)) = b_items[b_idx..]
.iter()
.enumerate()
.find(|(_idx, b)| a == *b)
{
b_idx += idx;
} else {
return false;
}
}
true
}
// Final blocks and best blocks should both be subsets of _all_ of the blocks reported.
assert!(
are_same_order_in(&bests, &alls),
"Best set {bests:?} should be a subset of all: {alls:?}"
);
assert!(
are_same_order_in(&finals, &alls),
"Final set {finals:?} should be a subset of all: {alls:?}"
);
Ok(())
}
#[subxt_test]
async fn finalized_headers_subscription() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let mut sub = api.blocks().subscribe_finalized().await?;
consume_initial_blocks(&mut sub).await;
// check that the finalized block reported lines up with the `latest_finalized_block_ref`.
for _ in 0..2 {
let header = sub.next().await.unwrap()?;
let finalized_hash = api.backend().latest_finalized_block_ref().await?.hash();
assert_eq!(header.hash(), finalized_hash);
}
Ok(())
}
#[subxt_test]
async fn missing_block_headers_will_be_filled_in() -> Result<(), subxt::Error> {
use subxt::backend::legacy;
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
// Manually subscribe to the next 6 finalized block headers, but deliberately
// filter out some in the middle so we get back b _ _ b _ b. This guarantees
// that there will be some gaps, even if there aren't any from the subscription.
let some_finalized_blocks = rpc
.chain_subscribe_finalized_heads()
.await?
.enumerate()
.take(6)
.filter(|(n, _)| {
let n = *n;
async move { n == 0 || n == 3 || n == 5 }
})
.map(|(_, r)| r);
// This should spot any gaps in the middle and fill them back in.
let all_finalized_blocks =
legacy::subscribe_to_block_headers_filling_in_gaps(rpc, some_finalized_blocks, None);
futures::pin_mut!(all_finalized_blocks);
// Iterate the block headers, making sure we get them all in order.
let mut last_block_number = None;
while let Some(header) = all_finalized_blocks.next().await {
let header = header?;
use subxt::config::Header;
let block_number: u128 = header.number().into();
if let Some(last) = last_block_number {
assert_eq!(last + 1, block_number);
}
last_block_number = Some(block_number);
}
assert!(last_block_number.is_some());
Ok(())
}
// Check that we can subscribe to non-finalized blocks.
#[subxt_test]
async fn runtime_api_call() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let rpc = ctx.legacy_rpc_methods().await;
let mut sub = api.blocks().subscribe_best().await?;
let block = sub.next().await.unwrap()?;
let rt = block.runtime_api().await;
// get metadata via raw state_call.
let meta_bytes = rt.call_raw("Metadata_metadata", None).await?;
let (_, meta1): (Compact<u32>, frame_metadata::RuntimeMetadataPrefixed) =
Decode::decode(&mut &*meta_bytes)?;
// get metadata via `state_getMetadata`.
let meta2_bytes = rpc.state_get_metadata(Some(block.hash())).await?.into_raw();
// They should be the same.
assert_eq!(meta1.encode(), meta2_bytes);
Ok(())
}
#[cfg(fullclient)]
#[subxt_test]
async fn fetch_block_and_decode_extrinsic_details() {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let bob = dev::bob();
// Setup; put an extrinsic into a block:
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob.public_key().into(), 10_000);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await
.unwrap();
let in_block = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized()
.await
.unwrap();
// Now, separately, download that block. Let's see what it contains..
let block_hash = in_block.block_hash();
let block = api.blocks().at(block_hash).await.unwrap();
// Ensure that we can clone the block.
let _ = block.clone();
let extrinsics = block.extrinsics().await.unwrap();
assert_eq!(extrinsics.block_hash(), block_hash);
// `.has` should work and find a transfer call.
assert!(
extrinsics
.has::<node_runtime::balances::calls::types::TransferAllowDeath>()
.unwrap()
);
// `.find_first` should similarly work to find the transfer call:
assert!(
extrinsics
.find_first::<node_runtime::balances::calls::types::TransferAllowDeath>()
.unwrap()
.is_some()
);
let block_extrinsics = extrinsics.iter().collect::<Vec<_>>();
let mut balance = None;
let mut timestamp = None;
for tx in block_extrinsics {
tx.as_root_extrinsic::<node_runtime::Call>().unwrap();
if let Some(ext) = tx
.as_extrinsic::<node_runtime::timestamp::calls::types::Set>()
.unwrap()
{
timestamp = Some((ext, tx.is_signed()));
}
if let Some(ext) = tx
.as_extrinsic::<node_runtime::balances::calls::types::TransferAllowDeath>()
.unwrap()
{
balance = Some((ext, tx.is_signed()));
}
}
// Check that we found the timestamp
{
let (_, is_signed) = timestamp.expect("Timestamp not found");
assert!(!is_signed);
}
// Check that we found the balance transfer
{
let (tx, is_signed) = balance.expect("Balance transfer not found");
assert_eq!(tx.value, 10_000);
assert!(is_signed);
}
}
/// A helper function to submit a transaction with some params and then get it back in a block,
/// so that we can test the decoding of it.
async fn submit_extrinsic_and_get_it_back(
api: &subxt::OnlineClient<SubstrateConfig>,
params: subxt::config::DefaultExtrinsicParamsBuilder<SubstrateConfig>,
) -> subxt::blocks::ExtrinsicDetails<SubstrateConfig, subxt::OnlineClient<SubstrateConfig>> {
let alice = dev::alice();
let bob = dev::bob();
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob.public_key().into(), 10_000);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, params.build())
.await
.unwrap();
let in_block = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized()
.await
.unwrap();
let block_hash = in_block.block_hash();
let block = api.blocks().at(block_hash).await.unwrap();
let extrinsics = block.extrinsics().await.unwrap();
extrinsics.iter().find(|e| e.is_signed()).unwrap()
}
#[cfg(fullclient)]
#[subxt_test]
async fn decode_transaction_extensions_from_blocks() {
let ctx = test_context().await;
let api = ctx.client();
let transaction1 =
submit_extrinsic_and_get_it_back(&api, DefaultExtrinsicParamsBuilder::new().tip(1234))
.await;
let extensions1 = transaction1.transaction_extensions().unwrap();
let nonce1 = extensions1.nonce().unwrap();
let nonce1_static = extensions1.find::<CheckNonce>().unwrap().unwrap();
let tip1 = extensions1.tip().unwrap();
let tip1_static: u128 = extensions1
.find::<ChargeAssetTxPayment<SubstrateConfig>>()
.unwrap()
.unwrap()
.tip();
let transaction2 =
submit_extrinsic_and_get_it_back(&api, DefaultExtrinsicParamsBuilder::new().tip(5678))
.await;
let extensions2 = transaction2.transaction_extensions().unwrap();
let nonce2 = extensions2.nonce().unwrap();
let nonce2_static = extensions2.find::<CheckNonce>().unwrap().unwrap();
let tip2 = extensions2.tip().unwrap();
let tip2_static: u128 = extensions2
.find::<ChargeAssetTxPayment<SubstrateConfig>>()
.unwrap()
.unwrap()
.tip();
assert_eq!(nonce1, 0);
assert_eq!(nonce1, nonce1_static);
assert_eq!(tip1, 1234);
assert_eq!(tip1, tip1_static);
assert_eq!(nonce2, 1);
assert_eq!(nonce2, nonce2_static);
assert_eq!(tip2, 5678);
assert_eq!(tip2, tip2_static);
let expected_transaction_extensions = [
"AuthorizeCall",
"CheckNonZeroSender",
"CheckSpecVersion",
"CheckTxVersion",
"CheckGenesis",
"CheckMortality",
"CheckNonce",
"CheckWeight",
"ChargeAssetTxPayment",
"CheckMetadataHash",
"EthSetOrigin",
"WeightReclaim",
];
assert_eq!(
extensions1.iter().count(),
expected_transaction_extensions.len()
);
for (e, expected_name) in extensions1
.iter()
.zip(expected_transaction_extensions.iter())
{
assert_eq!(e.name(), *expected_name);
}
assert_eq!(
extensions2.iter().count(),
expected_transaction_extensions.len()
);
for (e, expected_name) in extensions2
.iter()
.zip(expected_transaction_extensions.iter())
{
assert_eq!(e.name(), *expected_name);
}
}
#[cfg(fullclient)]
#[subxt_test]
async fn decode_block_mortality() {
let ctx = test_context().await;
let api = ctx.client();
// Explicit Immortal:
{
let tx =
submit_extrinsic_and_get_it_back(&api, DefaultExtrinsicParamsBuilder::new().immortal())
.await;
let mortality = tx
.transaction_extensions()
.unwrap()
.find::<CheckMortality<SubstrateConfig>>()
.unwrap()
.unwrap();
assert_eq!(mortality, Era::Immortal);
}
// Explicit Mortal:
for for_n_blocks in [16, 64, 128] {
let tx = submit_extrinsic_and_get_it_back(
&api,
DefaultExtrinsicParamsBuilder::new().mortal(for_n_blocks),
)
.await;
let mortality = tx
.transaction_extensions()
.unwrap()
.find::<CheckMortality<SubstrateConfig>>()
.unwrap()
.unwrap();
assert!(matches!(mortality, Era::Mortal {
period,
phase: _, // depends on current block so don't test it.
} if period == for_n_blocks));
}
// Implicitly, transactions should be mortal:
{
let tx =
submit_extrinsic_and_get_it_back(&api, DefaultExtrinsicParamsBuilder::default()).await;
let mortality = tx
.transaction_extensions()
.unwrap()
.find::<CheckMortality<SubstrateConfig>>()
.unwrap()
.unwrap();
assert!(matches!(
mortality,
Era::Mortal {
period: 32,
phase: _, // depends on current block so don't test it.
}
));
}
}
@@ -0,0 +1,245 @@
// 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.
//! Just sanity checking some of the new RPC methods to try and
//! catch differences as the implementations evolve.
use crate::{
subxt_test, test_context,
utils::{TestNodeProcess, node_runtime},
};
use codec::Encode;
use futures::{Stream, StreamExt};
use subxt::{
blocks::Block,
client::OnlineClient,
config::{Config, Hasher},
utils::AccountId32,
};
use pezkuwi_subxt_rpcs::methods::chain_head::{
ArchiveStorageEventItem, Bytes, StorageQuery, StorageQueryType,
};
use pezkuwi_subxt_signer::sr25519::dev;
async fn fetch_finalized_blocks<T: Config>(
ctx: &TestNodeProcess<T>,
n: usize,
) -> impl Stream<Item = Block<T, OnlineClient<T>>> {
ctx.client()
.blocks()
.subscribe_finalized()
.await
.expect("issue subscribing to finalized in fetch_finalized_blocks")
.skip(1) // <- skip first block in case next is close to being ready already.
.take(n)
.map(|r| r.expect("issue fetching block in fetch_finalized_blocks"))
}
#[subxt_test]
async fn archive_v1_body() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let mut blocks = fetch_finalized_blocks(&ctx, 3).await;
while let Some(block) = blocks.next().await {
let subxt_block_bodies = block
.extrinsics()
.await
.unwrap()
.iter()
.map(|e| e.bytes().to_vec());
let archive_block_bodies = rpc
.archive_v1_body(block.hash())
.await
.unwrap()
.into_iter()
.flatten()
.map(|e| e.0);
// chainHead and archive methods should return same block bodies
for (a, b) in subxt_block_bodies.zip(archive_block_bodies) {
assert_eq!(a, b);
}
}
}
#[subxt_test]
async fn archive_v1_call() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let mut blocks = fetch_finalized_blocks(&ctx, 3).await;
while let Some(block) = blocks.next().await {
let pezkuwi_subxt_metadata_versions = block
.runtime_api()
.await
.call(node_runtime::apis().metadata().metadata_versions())
.await
.unwrap()
.encode();
let archive_metadata_versions = rpc
.archive_v1_call(block.hash(), "Metadata_metadata_versions", &[])
.await
.unwrap()
.as_success()
.unwrap()
.0;
assert_eq!(pezkuwi_subxt_metadata_versions, archive_metadata_versions);
}
}
#[subxt_test]
async fn archive_v1_finalized_height() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
// This test is quite ugly. Originally, we asked for finalized blocks from subxt and
// asserted that the archive height we then get back matches, but that is subject to
// races between subxt's stream and reality (and failed surprisingly often). To try
// to avoid this, we weaken the test to just check that the height increments over time.
let mut last_block_height = None;
loop {
// Fetch archive block height.
let archive_block_height = rpc.archive_v1_finalized_height().await.unwrap();
// On a dev node we expect blocks to be finalized 1 by 1, so panic
// if the height we fetch has grown by more than 1.
if let Some(last) = last_block_height {
if archive_block_height != last && archive_block_height != last + 1 {
panic!(
"Archive block height should increase 1 at a time, but jumped from {last} to {archive_block_height}"
);
}
}
last_block_height = Some(archive_block_height);
if archive_block_height > 5 {
break;
}
// Wait a little before looping
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
}
#[subxt_test]
async fn archive_v1_genesis_hash() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let chain_head_genesis_hash = rpc.chainspec_v1_genesis_hash().await.unwrap();
let archive_genesis_hash = rpc.archive_v1_genesis_hash().await.unwrap();
assert_eq!(chain_head_genesis_hash, archive_genesis_hash);
}
#[subxt_test]
async fn archive_v1_hash_by_height() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let mut blocks = fetch_finalized_blocks(&ctx, 3).await;
while let Some(block) = blocks.next().await {
let subxt_block_height = block.number() as usize;
let subxt_block_hash = block.hash();
let archive_block_hash = rpc
.archive_v1_hash_by_height(subxt_block_height)
.await
.unwrap();
// Should only ever be 1 finalized block hash.
assert_eq!(archive_block_hash.len(), 1);
assert_eq!(subxt_block_hash, archive_block_hash[0]);
}
}
#[subxt_test]
async fn archive_v1_header() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let mut blocks = fetch_finalized_blocks(&ctx, 3).await;
while let Some(block) = blocks.next().await {
let block_hash = block.hash();
let subxt_block_header = block.header();
let archive_block_header = rpc.archive_v1_header(block_hash).await.unwrap().unwrap();
assert_eq!(subxt_block_header, &archive_block_header);
}
}
#[subxt_test]
async fn archive_v1_storage() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let api = ctx.client();
let hasher = api.hasher();
let mut blocks = fetch_finalized_blocks(&ctx, 3).await;
while let Some(block) = blocks.next().await {
let block_hash = block.hash();
let alice: AccountId32 = dev::alice().public_key().into();
let addr = node_runtime::storage().system().account();
// Fetch value using Subxt to compare against
let storage_at = api.storage().at(block.reference());
let storage_entry = storage_at.entry(addr).unwrap();
let subxt_account_info = storage_entry.fetch((alice.clone(),)).await.unwrap();
let subxt_account_info_bytes = subxt_account_info.bytes();
let account_info_addr = storage_entry.key((alice,)).unwrap();
// Construct archive query; ask for item then hash of item.
let storage_query = vec![
StorageQuery {
key: account_info_addr.as_slice(),
query_type: StorageQueryType::Value,
},
StorageQuery {
key: account_info_addr.as_slice(),
query_type: StorageQueryType::Hash,
},
];
let mut res = rpc
.archive_v1_storage(block_hash, storage_query, None)
.await
.unwrap();
// Expect item back first in archive response
let query_item = res.next().await.unwrap().unwrap().as_item().unwrap();
assert_eq!(
query_item,
ArchiveStorageEventItem {
key: Bytes(account_info_addr.clone()),
value: Some(Bytes(subxt_account_info_bytes.to_vec())),
hash: None,
closest_descendant_merkle_value: None,
child_trie_key: None
}
);
// Expect item hash back next
let query_item_hash = res.next().await.unwrap().unwrap().as_item().unwrap();
assert_eq!(
query_item_hash,
ArchiveStorageEventItem {
key: Bytes(account_info_addr),
value: None,
hash: Some(hasher.hash(subxt_account_info_bytes)),
closest_descendant_merkle_value: None,
child_trie_key: None
}
);
// Expect nothing else back after
assert!(res.next().await.unwrap().unwrap().is_done());
assert!(res.next().await.is_none());
}
}
@@ -0,0 +1,431 @@
// 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.
//! Just sanity checking some of the new RPC methods to try and
//! catch differences as the implementations evolve.
use crate::{
subxt_test, test_context,
utils::{consume_initial_blocks, node_runtime},
};
use assert_matches::assert_matches;
use codec::Encode;
use futures::Stream;
use subxt::{
config::Hasher,
utils::{AccountId32, MultiAddress},
};
use pezkuwi_subxt_rpcs::methods::chain_head::{
FollowEvent, Initialized, MethodResponse, RuntimeEvent, RuntimeVersionEvent, StorageQuery,
StorageQueryType,
};
use pezkuwi_subxt_signer::sr25519::dev;
#[subxt_test]
async fn chainhead_v1_follow() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let legacy_rpc = ctx.legacy_rpc_methods().await;
// Check subscription with runtime updates set on false.
let mut blocks = rpc.chainhead_v1_follow(false).await.unwrap();
let event = blocks.next().await.unwrap().unwrap();
// The initialized event should contain the finalized block hash.
let finalized_block_hash = legacy_rpc.chain_get_finalized_head().await.unwrap();
assert_matches!(
event,
FollowEvent::Initialized(Initialized { finalized_block_hashes, finalized_block_runtime }) => {
assert!(finalized_block_hashes.contains(&finalized_block_hash));
assert!(finalized_block_runtime.is_none());
}
);
// Expect subscription to produce runtime versions.
let mut blocks = rpc.chainhead_v1_follow(true).await.unwrap();
let event = blocks.next().await.unwrap().unwrap();
// The initialized event should contain the finalized block hash.
let finalized_block_hash = legacy_rpc.chain_get_finalized_head().await.unwrap();
let runtime_version = ctx.client().runtime_version();
assert_matches!(
event,
FollowEvent::Initialized(init) => {
assert!(init.finalized_block_hashes.contains(&finalized_block_hash));
if let Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec })) = init.finalized_block_runtime {
assert_eq!(spec.spec_version, runtime_version.spec_version);
assert_eq!(spec.transaction_version, runtime_version.transaction_version);
} else {
panic!("runtime details not provided with init event, got {:?}", init.finalized_block_runtime);
}
}
);
}
#[subxt_test]
async fn chainhead_v1_body() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let mut blocks = rpc.chainhead_v1_follow(false).await.unwrap();
let event = blocks.next().await.unwrap().unwrap();
let hash = match event {
FollowEvent::Initialized(init) => *init.finalized_block_hashes.last().unwrap(),
_ => panic!("Unexpected event"),
};
let sub_id = blocks.subscription_id().unwrap();
// Fetch the block's body.
let response = rpc.chainhead_v1_body(sub_id, hash).await.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Response propagated to `chainHead_follow`.
let event = next_operation_event(&mut blocks).await;
assert_matches!(
event,
FollowEvent::OperationBodyDone(done) if done.operation_id == operation_id
);
}
#[subxt_test]
async fn chainhead_v1_header() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let legacy_rpc = ctx.legacy_rpc_methods().await;
let mut blocks = rpc.chainhead_v1_follow(false).await.unwrap();
let event = blocks.next().await.unwrap().unwrap();
let hash = match event {
FollowEvent::Initialized(init) => *init.finalized_block_hashes.last().unwrap(),
_ => panic!("Unexpected event"),
};
let sub_id = blocks.subscription_id().unwrap();
let new_header = legacy_rpc
.chain_get_header(Some(hash))
.await
.unwrap()
.unwrap();
let old_header = rpc
.chainhead_v1_header(sub_id, hash)
.await
.unwrap()
.unwrap();
assert_eq!(new_header, old_header);
}
#[subxt_test]
async fn chainhead_v1_storage() {
let ctx = test_context().await;
let api = ctx.client();
let rpc = ctx.chainhead_rpc_methods().await;
let mut blocks = rpc.chainhead_v1_follow(false).await.unwrap();
let event = blocks.next().await.unwrap().unwrap();
let hash = match event {
FollowEvent::Initialized(init) => *init.finalized_block_hashes.last().unwrap(),
_ => panic!("Unexpected event"),
};
let sub_id = blocks.subscription_id().unwrap();
let alice: AccountId32 = dev::alice().public_key().into();
let addr_bytes = {
let storage_at = api.storage().at_latest().await.unwrap();
let addr = node_runtime::storage().system().account();
storage_at.entry(addr).unwrap().key((alice,)).unwrap()
};
let items = vec![StorageQuery {
key: addr_bytes.as_slice(),
query_type: StorageQueryType::Value,
}];
// Fetch storage.
let response = rpc
.chainhead_v1_storage(sub_id, hash, items, None)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Response propagated to `chainHead_follow`.
let event = next_operation_event(&mut blocks).await;
assert_matches!(
event,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key.0 == addr_bytes
);
let event = next_operation_event(&mut blocks).await;
assert_matches!(event, FollowEvent::OperationStorageDone(res) if res.operation_id == operation_id);
}
#[subxt_test]
async fn chainhead_v1_call() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let mut blocks = rpc.chainhead_v1_follow(true).await.unwrap();
let event = blocks.next().await.unwrap().unwrap();
let hash = match event {
FollowEvent::Initialized(init) => *init.finalized_block_hashes.last().unwrap(),
_ => panic!("Unexpected event"),
};
let sub_id = blocks.subscription_id().unwrap();
let alice_id = dev::alice().public_key().to_account_id();
// Runtime API call.
let response = rpc
.chainhead_v1_call(
sub_id,
hash,
"AccountNonceApi_account_nonce",
&alice_id.encode(),
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Response propagated to `chainHead_follow`.
let event = next_operation_event(&mut blocks).await;
assert_matches!(
event,
FollowEvent::OperationCallDone(res) if res.operation_id == operation_id
);
}
#[subxt_test]
async fn chainhead_v1_unpin() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
let mut blocks = rpc.chainhead_v1_follow(true).await.unwrap();
let event = blocks.next().await.unwrap().unwrap();
let hash = match event {
FollowEvent::Initialized(init) => *init.finalized_block_hashes.last().unwrap(),
_ => panic!("Unexpected event"),
};
let sub_id = blocks.subscription_id().unwrap();
assert!(rpc.chainhead_v1_unpin(sub_id, hash).await.is_ok());
// The block was already unpinned.
assert!(rpc.chainhead_v1_unpin(sub_id, hash).await.is_err());
}
#[cfg(fullclient)]
#[subxt_test]
async fn chainspec_v1_genesishash() {
let ctx = test_context().await;
let old_rpc = ctx.legacy_rpc_methods().await;
let rpc = ctx.chainhead_rpc_methods().await;
let a = old_rpc.genesis_hash().await.unwrap();
let b = rpc.chainspec_v1_genesis_hash().await.unwrap();
assert_eq!(a, b);
}
#[cfg(fullclient)]
#[subxt_test]
async fn chainspec_v1_chainname() {
let ctx = test_context().await;
let old_rpc = ctx.legacy_rpc_methods().await;
let rpc = ctx.chainhead_rpc_methods().await;
let a = old_rpc.system_chain().await.unwrap();
let b = rpc.chainspec_v1_chain_name().await.unwrap();
assert_eq!(a, b);
}
#[cfg(fullclient)]
#[subxt_test]
async fn chainspec_v1_properties() {
let ctx = test_context().await;
let old_rpc = ctx.legacy_rpc_methods().await;
let rpc = ctx.chainhead_rpc_methods().await;
let a = old_rpc.system_properties().await.unwrap();
let b = rpc.chainspec_v1_properties().await.unwrap();
assert_eq!(a, b);
}
#[cfg(fullclient)]
#[subxt_test]
async fn transactionwatch_v1_submit_and_watch() {
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
// Build and sign some random tx, just to get some appropriate bytes:
let payload = node_runtime::tx().system().remark(b"hello".to_vec());
let tx_bytes = ctx
.client()
.tx()
.create_partial_offline(&payload, Default::default())
.unwrap()
.sign(&dev::alice())
.into_encoded();
// Test submitting it:
let mut sub = rpc
.transactionwatch_v1_submit_and_watch(&tx_bytes)
.await
.unwrap();
// Check that the messages we get back on the way to it finishing deserialize ok
// (this will miss some cases).
while let Some(_ev) = sub.next().await.transpose().unwrap() {
// This stream should end when it hits the relevant stopping event.
// If the test continues forever then something isn't working.
// If we hit an error then that's also an issue!
}
}
/// Ignore block related events and obtain the next event related to an operation.
async fn next_operation_event<
T: serde::de::DeserializeOwned,
S: Unpin + Stream<Item = Result<FollowEvent<T>, E>>,
E: core::fmt::Debug,
>(
sub: &mut S,
) -> FollowEvent<T> {
use futures::StreamExt;
// Number of events to wait for the next operation event.
const NUM_EVENTS: usize = 10;
for _ in 0..NUM_EVENTS {
let event = sub.next().await.unwrap().unwrap();
match event {
// Can also return the `Stop` event for better debugging.
FollowEvent::Initialized(_)
| FollowEvent::NewBlock(_)
| FollowEvent::BestBlockChanged(_)
| FollowEvent::Finalized(_) => continue,
_ => (),
};
return event;
}
panic!("Cannot find operation related event after {NUM_EVENTS} produced events");
}
#[tokio::test]
async fn transaction_v1_broadcast() {
let bob = dev::bob();
let bob_address: MultiAddress<AccountId32, u32> = bob.public_key().into();
let ctx = test_context().await;
let api = ctx.client();
let hasher = api.hasher();
let rpc = ctx.chainhead_rpc_methods().await;
let tx_payload = node_runtime::tx()
.balances()
.transfer_allow_death(bob_address.clone(), 10_001);
let tx = ctx
.client()
.tx()
.create_partial_offline(&tx_payload, Default::default())
.unwrap()
.sign(&dev::alice());
let tx_hash = tx.hash();
let tx_bytes = tx.into_encoded();
// Subscribe to finalized blocks.
let mut finalized_sub = api.blocks().subscribe_finalized().await.unwrap();
consume_initial_blocks(&mut finalized_sub).await;
// Expect the tx to be encountered in a maximum number of blocks.
let mut num_blocks: usize = 20;
// Submit the transaction.
let _operation_id = rpc
.transaction_v1_broadcast(&tx_bytes)
.await
.unwrap()
.expect("Server is not overloaded by 1 tx; qed");
while let Some(finalized) = finalized_sub.next().await {
let finalized = finalized.unwrap();
// Started with positive, should not overflow.
num_blocks = num_blocks.saturating_sub(1);
if num_blocks == 0 {
panic!("Did not find the tx in due time");
}
let extrinsics = finalized.extrinsics().await.unwrap();
let block_extrinsics = extrinsics.iter().collect::<Vec<_>>();
let Some(ext) = block_extrinsics
.iter()
.find(|ext| hasher.hash(ext.bytes()) == tx_hash)
else {
continue;
};
let ext = ext
.as_extrinsic::<node_runtime::balances::calls::types::TransferAllowDeath>()
.unwrap()
.unwrap();
assert_eq!(ext.value, 10_001);
return;
}
}
#[tokio::test]
async fn transaction_v1_stop() {
let bob = dev::bob();
let bob_address: MultiAddress<AccountId32, u32> = bob.public_key().into();
let ctx = test_context().await;
let rpc = ctx.chainhead_rpc_methods().await;
// Cannot stop an operation that was not started.
let _err = rpc
.transaction_v1_stop("non-existent-operation-id")
.await
.unwrap_err();
// Submit a transaction and stop it.
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob_address.clone(), 10_001);
let tx_bytes = ctx
.client()
.tx()
.create_partial_offline(&tx, Default::default())
.unwrap()
.sign(&dev::alice())
.into_encoded();
// Submit the transaction.
let operation_id = rpc
.transaction_v1_broadcast(&tx_bytes)
.await
.unwrap()
.expect("Server is not overloaded by 1 tx; qed");
rpc.transaction_v1_stop(&operation_id).await.unwrap();
// Cannot stop it twice.
let _err = rpc.transaction_v1_stop(&operation_id).await.unwrap_err();
}
@@ -0,0 +1,136 @@
// 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.
//! Just sanity checking some of the legacy RPC methods to make
//! sure they don't error out and can decode their results OK.
use crate::{subxt_test, test_context};
#[subxt_test]
async fn chain_get_block_hash() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
rpc.chain_get_block_hash(None).await.unwrap();
}
#[subxt_test]
async fn chain_get_block() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let hash = rpc.chain_get_block_hash(None).await.unwrap();
rpc.chain_get_block(hash).await.unwrap();
}
#[subxt_test]
async fn chain_get_finalized_head() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
rpc.chain_get_finalized_head().await.unwrap();
}
#[subxt_test]
async fn chain_subscribe_all_heads() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let mut sub = rpc.chain_subscribe_all_heads().await.unwrap();
let _block_header = sub.next().await.unwrap().unwrap();
}
#[subxt_test]
async fn chain_subscribe_finalized_heads() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let mut sub = rpc.chain_subscribe_finalized_heads().await.unwrap();
let _block_header = sub.next().await.unwrap().unwrap();
}
#[subxt_test]
async fn chain_subscribe_new_heads() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let mut sub = rpc.chain_subscribe_new_heads().await.unwrap();
let _block_header = sub.next().await.unwrap().unwrap();
}
#[subxt_test]
async fn genesis_hash() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let _genesis_hash = rpc.genesis_hash().await.unwrap();
}
#[subxt_test]
async fn state_get_metadata() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let _metadata = rpc.state_get_metadata(None).await.unwrap();
}
#[subxt_test]
async fn state_call() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let _metadata = rpc
.state_call("Metadata_metadata", None, None)
.await
.unwrap();
}
#[subxt_test]
async fn system_health() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let _ = rpc.system_health().await.unwrap();
}
#[subxt_test]
async fn system_chain() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let _ = rpc.system_chain().await.unwrap();
}
#[subxt_test]
async fn system_name() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let _ = rpc.system_name().await.unwrap();
}
#[subxt_test]
async fn system_version() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let _ = rpc.system_version().await.unwrap();
}
#[subxt_test]
async fn system_chain_type() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let chain_type = rpc.system_chain_type().await.unwrap();
assert_eq!(chain_type, "Development");
}
#[subxt_test]
async fn system_properties() {
let ctx = test_context().await;
let rpc = ctx.legacy_rpc_methods().await;
let _ = rpc.system_properties().await.unwrap();
}
@@ -0,0 +1,455 @@
// 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::{
subxt_test, test_context,
utils::{node_runtime, wait_for_blocks},
};
use codec::{Decode, Encode};
#[cfg(fullclient)]
use futures::StreamExt;
use subxt::{
backend::BackendExt,
error::{DispatchError, TransactionEventsError, TransactionFinalizedSuccessError},
tx::{TransactionInvalid, ValidationResult},
};
use pezkuwi_subxt_signer::sr25519::dev;
#[cfg(fullclient)]
mod archive_rpcs;
#[cfg(fullclient)]
mod legacy_rpcs;
mod chain_head_rpcs;
#[cfg(fullclient)]
#[subxt_test]
async fn storage_iter() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let addr = node_runtime::storage().system().account();
let storage = api.storage().at_latest().await.unwrap();
let entry = storage.entry(addr)?;
let len = entry
.iter(())
.await
.unwrap()
.filter_map(async |r| r.ok())
.count()
.await;
assert_eq!(len, 17);
Ok(())
}
#[cfg(fullclient)]
#[subxt_test]
async fn storage_child_values_same_across_backends() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let chainhead_client = ctx.chainhead_backend().await;
let legacy_client = ctx.legacy_backend().await;
let addr = node_runtime::storage().system().account();
let block_ref = legacy_client
.blocks()
.at_latest()
.await
.unwrap()
.reference();
let chainhead_storage = chainhead_client.storage().at(block_ref.clone());
let a: Vec<_> = chainhead_storage
.iter(&addr, ())
.await
.unwrap()
.collect()
.await;
let legacy_storage = legacy_client.storage().at(block_ref.clone());
let b: Vec<_> = legacy_storage
.iter(&addr, ())
.await
.unwrap()
.collect()
.await;
for (a, b) in a.into_iter().zip(b.into_iter()) {
let a = a.unwrap();
let b = b.unwrap();
assert_eq!(a.key_bytes(), b.key_bytes());
assert_eq!(a.value().bytes(), b.value().bytes());
}
Ok(())
}
#[subxt_test]
async fn transaction_validation() {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let bob = dev::bob();
wait_for_blocks(&api).await;
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob.public_key().into(), 10_000);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await
.unwrap();
signed_extrinsic
.validate()
.await
.expect("validation failed");
signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await
.unwrap();
}
#[subxt_test]
async fn validation_fails() {
use std::str::FromStr;
use pezkuwi_subxt_signer::{SecretUri, sr25519::Keypair};
let ctx = test_context().await;
let api = ctx.client();
wait_for_blocks(&api).await;
let from = Keypair::from_uri(&SecretUri::from_str("//AccountWithNoFunds").unwrap()).unwrap();
let to = dev::bob();
// The actual TX is not important; the account has no funds to pay for it.
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(to.public_key().into(), 1);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &from, Default::default())
.await
.unwrap();
let validation_res = signed_extrinsic
.validate()
.await
.expect("dryrunning failed");
assert_eq!(
validation_res,
ValidationResult::Invalid(TransactionInvalid::Payment)
);
}
#[subxt_test]
async fn external_signing() {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
// Create a partial extrinsic. We can get the signer payload at this point, to be
// signed externally.
let tx = node_runtime::tx().preimage().note_preimage(vec![0u8]);
let mut partial_extrinsic = api
.tx()
.create_partial(&tx, &alice.public_key().into(), Default::default())
.await
.unwrap();
// Get the signer payload.
let signer_payload = partial_extrinsic.signer_payload();
// Sign it (possibly externally).
let signature = alice.sign(&signer_payload);
// Use this to build a signed extrinsic.
let extrinsic = partial_extrinsic
.sign_with_account_and_signature(&alice.public_key().into(), &signature.into());
// And now submit it.
extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await
.unwrap();
}
#[cfg(fullclient)]
// TODO: Investigate and fix this test failure when using the ChainHeadBackend.
// (https://github.com/paritytech/subxt/issues/1308)
#[cfg(legacy_backend)]
#[subxt_test]
async fn submit_large_extrinsic() {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
// 2 MiB blob of data.
let bytes = vec![0_u8; 2 * 1024 * 1024];
// The preimage pallet allows storing and managing large byte-blobs.
let tx = node_runtime::tx().preimage().note_preimage(bytes);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await
.unwrap();
signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await
.unwrap();
}
#[subxt_test]
async fn decode_a_module_error() {
use node_runtime::runtime_types::pallet_assets::pallet as assets;
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let alice_addr = alice.public_key().into();
// Trying to work with an asset ID 1 which doesn't exist should return an
// "unknown" module error from the assets pallet.
let freeze_unknown_asset = node_runtime::tx().assets().freeze(1, alice_addr);
let signed_extrinsic = api
.tx()
.create_signed(&freeze_unknown_asset, &alice, Default::default())
.await
.unwrap();
let err = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await
.expect_err("an 'unknown asset' error");
let TransactionFinalizedSuccessError::SuccessError(TransactionEventsError::ExtrinsicFailed(
DispatchError::Module(module_err),
)) = err
else {
panic!("Expected a ModuleError, got {err:?}");
};
// Decode the error into our generated Error type.
let decoded_err = module_err.as_root_error::<node_runtime::Error>().unwrap();
// Decoding should result in an Assets.Unknown error:
assert_eq!(
decoded_err,
node_runtime::Error::Assets(assets::Error::Unknown)
);
}
#[subxt_test]
async fn unsigned_extrinsic_is_same_shape_as_polkadotjs() {
let ctx = test_context().await;
let api = ctx.client();
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(dev::alice().public_key().into(), 12345000000000000);
let actual_tx = api.tx().create_unsigned(&tx).unwrap();
let actual_tx_bytes = actual_tx.encoded();
// How these were obtained:
// - start local substrate node.
// - open polkadot.js UI in browser and point at local node.
// - open dev console (may need to refresh page now) and find the WS connection.
// - create a balances.transferAllowDeath to ALICE (doesn't matter who from) with 12345 and "submit unsigned".
// - find the submitAndWatchExtrinsic call in the WS connection to get these bytes:
let expected_tx_bytes = hex::decode(
"b004060000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0f0090c04bb6db2b"
)
.unwrap();
// Make sure our encoding is the same as the encoding polkadot UI created.
assert_eq!(actual_tx_bytes, expected_tx_bytes);
}
#[subxt_test]
async fn extrinsic_hash_is_same_as_returned() {
let ctx = test_context().await;
let api = ctx.client();
let rpc = ctx.legacy_rpc_methods().await;
let payload = node_runtime::tx()
.balances()
.transfer_allow_death(dev::alice().public_key().into(), 12345000000000000);
let tx = api
.tx()
.create_signed(&payload, &dev::bob(), Default::default())
.await
.unwrap();
// 1. Calculate the hash locally:
let local_hash = tx.hash();
// 2. Submit and get the hash back from the node:
let external_hash = rpc.author_submit_extrinsic(tx.encoded()).await.unwrap();
assert_eq!(local_hash, external_hash);
}
/// taken from original type <https://docs.rs/pallet-transaction-payment/latest/pallet_transaction_payment/struct.FeeDetails.html>
#[derive(Encode, Decode, Debug, Clone, Eq, PartialEq)]
pub struct FeeDetails {
/// The minimum fee for a transaction to be included in a block.
pub inclusion_fee: Option<InclusionFee>,
/// tip
pub tip: u128,
}
/// taken from original type <https://docs.rs/pallet-transaction-payment/latest/pallet_transaction_payment/struct.InclusionFee.html>
/// The base fee and adjusted weight and length fees constitute the _inclusion fee_.
#[derive(Encode, Decode, Debug, Clone, Eq, PartialEq)]
pub struct InclusionFee {
/// minimum amount a user pays for a transaction.
pub base_fee: u128,
/// amount paid for the encoded length (in bytes) of the transaction.
pub len_fee: u128,
///
/// - `targeted_fee_adjustment`: This is a multiplier that can tune the final fee based on the
/// congestion of the network.
/// - `weight_fee`: This amount is computed based on the weight of the transaction. Weight
/// accounts for the execution time of a transaction.
///
/// adjusted_weight_fee = targeted_fee_adjustment * weight_fee
pub adjusted_weight_fee: u128,
}
#[subxt_test]
async fn partial_fee_estimate_correct() {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let bob = dev::bob();
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob.public_key().into(), 1_000_000_000_000);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await
.unwrap();
// Method I: TransactionPaymentApi_query_info
let partial_fee_1 = signed_extrinsic.partial_fee_estimate().await.unwrap();
// Method II: TransactionPaymentApi_query_fee_details + calculations
let latest_block_ref = api.backend().latest_finalized_block_ref().await.unwrap();
let len_bytes: [u8; 4] = (signed_extrinsic.encoded().len() as u32).to_le_bytes();
let encoded_with_len = [signed_extrinsic.encoded(), &len_bytes[..]].concat();
let InclusionFee {
base_fee,
len_fee,
adjusted_weight_fee,
} = api
.backend()
.call_decoding::<FeeDetails>(
"TransactionPaymentApi_query_fee_details",
Some(&encoded_with_len),
latest_block_ref.hash(),
)
.await
.unwrap()
.inclusion_fee
.unwrap();
let partial_fee_2 = base_fee + len_fee + adjusted_weight_fee;
// Both methods should yield the same fee
assert_eq!(partial_fee_1, partial_fee_2);
}
// This test runs OK locally but fails sporadically in CI eg:
//
// https://github.com/paritytech/subxt/actions/runs/13374953009/job/37353887719?pr=1910#step:7:178
// https://github.com/paritytech/subxt/actions/runs/13385878645/job/37382498200#step:6:163
//
// While those errors were timeouts, I also saw errors like "intersections size is 1".
/*
#[subxt_test(timeout = 300)]
async fn chainhead_block_subscription_reconnect() {
use std::collections::HashSet;
use crate::test_context_reconnecting_rpc_client;
let ctx = test_context_reconnecting_rpc_client().await;
let api = ctx.chainhead_backend().await;ccc
let chainhead_client_blocks = move |num: usize| {
let api = api.clone();
async move {
let mut missed_blocks = false;
let blocks =
// Ignore `disconnected events`.
// This will be emitted by the legacy backend for every reconnection.
api.blocks().subscribe_finalized().await.unwrap().filter(|item| {
let disconnected = match item {
Ok(_) => false,
Err(e) => {
if e.is_disconnected_will_reconnect() && e.to_string().contains("Missed at least one block when the connection was lost") {
missed_blocks = true;
}
e.is_disconnected_will_reconnect()
}
};
futures::future::ready(!disconnected)
})
.take(num)
.map(|x| x.unwrap().hash().to_string())
.collect::<Vec<String>>().await;
(blocks, missed_blocks)
}
};
let (blocks, _) = chainhead_client_blocks(3).await;
let blocks: HashSet<String> = HashSet::from_iter(blocks.into_iter());
assert!(blocks.len() == 3);
let ctx = ctx.restart().await;
// Make client aware that connection was dropped and force them to reconnect
let _ = ctx.chainhead_backend().await.backend().genesis_hash().await;
let (unstable_blocks, blocks_missed) = chainhead_client_blocks(6).await;
if !blocks_missed {
let unstable_blocks: HashSet<String> = HashSet::from_iter(unstable_blocks.into_iter());
let intersection = unstable_blocks.intersection(&blocks).count();
assert!(intersection >= 3, "intersections size is {}", intersection);
}
}
*/
@@ -0,0 +1,167 @@
// 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 codec::Decode;
use regex::Regex;
use pezkuwi_subxt_codegen::{CodegenBuilder, syn};
use pezkuwi_subxt_metadata::Metadata;
fn load_test_metadata() -> Metadata {
let bytes = test_runtime::METADATA;
Metadata::decode(&mut &*bytes).expect("Cannot decode scale metadata")
}
fn metadata_docs() -> Vec<String> {
// Load the runtime metadata downloaded from a node via `test-runtime`.
let metadata = load_test_metadata();
// Inspect the metadata types and collect the documentation.
let mut docs = Vec::new();
for ty in &metadata.types().types {
docs.extend_from_slice(&ty.ty.docs);
}
for pallet in metadata.pallets() {
if let Some(storage) = pallet.storage() {
for entry in storage.entries() {
docs.extend_from_slice(entry.docs());
}
}
// Note: Calls, Events and Errors are deduced directly to
// PortableTypes which are handled above.
for constant in pallet.constants() {
docs.extend_from_slice(constant.docs());
}
}
// Note: Extrinsics do not have associated documentation, but is implied by
// associated Type.
// Inspect the runtime API types and collect the documentation.
for api in metadata.runtime_api_traits() {
docs.extend_from_slice(api.docs());
for method in api.methods() {
docs.extend_from_slice(method.docs());
}
}
docs
}
fn generate_runtime_interface(should_gen_docs: bool) -> String {
// Load the runtime metadata downloaded from a node via `test-runtime`.
let metadata = load_test_metadata();
let mut codegen = CodegenBuilder::new();
if !should_gen_docs {
codegen.no_docs();
}
codegen
.generate(metadata)
.expect("API generation must be valid")
.to_string()
}
fn interface_docs(should_gen_docs: bool) -> Vec<String> {
// Generate the runtime interface from the node's metadata.
// Note: the API is generated on a single line.
let runtime_api = generate_runtime_interface(should_gen_docs);
// Documentation lines have the following format:
// # [ doc = "Upward message is invalid XCM."]
// Given the API is generated on a single line, the regex matching
// must be lazy hence the `?` in the matched group `(.*?)`.
//
// The greedy `non-?` matching would lead to one single match
// from the beginning of the first documentation tag, containing everything up to
// the last documentation tag
// `# [ doc = "msg"] # [ doc = "msg2"] ... api ... # [ doc = "msgN" ]`
//
// The `(.*?)` stands for match any character zero or more times lazily.
let re = Regex::new(r#"\# \[doc = "(.*?)"\]"#).unwrap();
re.captures_iter(&runtime_api)
.filter_map(|capture| {
// Get the matched group (ie index 1).
capture.get(1).as_ref().map(|doc| {
// Generated documentation will escape special characters.
// Replace escaped characters with unescaped variants for
// exact matching on the raw metadata documentation.
doc.as_str()
.replace("\\n", "\n")
.replace("\\t", "\t")
.replace("\\\"", "\"")
})
})
.collect()
}
#[test]
fn check_documentation() {
// Inspect metadata and obtain all associated documentation.
let raw_docs = metadata_docs();
// Obtain documentation from the generated API.
let runtime_docs = interface_docs(true);
for raw in raw_docs.iter() {
if raw.contains(|c: char| !c.is_ascii()) {
// Ignore lines containing on-ascii chars; they are encoded currently
// as "\u{nn}" which doesn't match their input which is the raw non-ascii
// char.
continue;
}
assert!(
runtime_docs.contains(raw),
"Documentation not present in runtime API: {raw}"
);
}
}
#[test]
fn check_no_documentation() {
// Inspect metadata and obtain all associated documentation.
let raw_docs = metadata_docs();
// Obtain documentation from the generated API.
let runtime_docs = interface_docs(false);
for raw in raw_docs.iter() {
assert!(
!runtime_docs.contains(raw),
"Documentation should not be present in runtime API: {raw}"
);
}
}
#[test]
fn check_root_attrs_preserved() {
let metadata = load_test_metadata();
// Test that the root docs/attr are preserved.
let item_mod = syn::parse_quote!(
/// Some root level documentation
#[some_root_attribute]
pub mod api {}
);
let mut codegen = CodegenBuilder::new();
codegen.set_target_module(item_mod);
let generated_code = codegen
.generate(metadata)
.expect("API generation must be valid")
.to_string();
let doc_str_loc = generated_code
.find("Some root level documentation")
.expect("root docs should be preserved");
let attr_loc = generated_code
.find("some_root_attribute") // '#' is space separated in generated output.
.expect("root attr should be preserved");
let mod_start = generated_code
.find("pub mod api")
.expect("'pub mod api' expected");
// These things should be before the mod start
assert!(doc_str_loc < mod_start);
assert!(attr_loc < mod_start);
}
@@ -0,0 +1,17 @@
// 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.
/// Checks that code generated by `subxt-cli codegen` compiles. Allows inspection of compiler errors
/// directly, more accurately than via the macro and `cargo expand`.
///
/// Generate by running this at the root of the repository:
///
/// ```text
/// cargo run --bin subxt -- codegen --file artifacts/polkadot_metadata_full.scale | rustfmt > testing/integration-tests/src/full_client/codegen/polkadot.rs
/// ```
#[rustfmt::skip]
#[allow(clippy::all)]
mod polkadot;
mod documentation;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,395 @@
// 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::{
node_runtime::{self, balances, system},
subxt_test, test_context,
};
use codec::Decode;
use subxt::{
error::{DispatchError, TokenError, TransactionEventsError, TransactionFinalizedSuccessError},
ext::scale_decode::DecodeAsType,
utils::{AccountId32, MultiAddress},
};
use pezkuwi_subxt_signer::sr25519::dev;
#[subxt_test]
async fn tx_basic_transfer() -> Result<(), subxt::Error> {
let alice = dev::alice();
let bob = dev::bob();
let bob_address = bob.public_key().to_address();
let ctx = test_context().await;
let api = ctx.client();
let account_addr = node_runtime::storage().system().account();
let storage_at_pre = api.storage().at_latest().await?;
let account_entry_pre = storage_at_pre.entry(&account_addr)?;
let alice_pre = account_entry_pre
.fetch((alice.public_key().to_account_id(),))
.await?
.decode()?;
let bob_pre = account_entry_pre
.fetch((bob.public_key().to_account_id(),))
.await?
.decode()?;
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob_address, 10_000);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await?;
let events = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await?;
let event = events
.find_first::<balances::events::Transfer>()
.expect("Failed to decode balances::events::Transfer")
.expect("Failed to find balances::events::Transfer");
let _extrinsic_success = events
.find_first::<system::events::ExtrinsicSuccess>()
.expect("Failed to decode ExtrinisicSuccess")
.expect("Failed to find ExtrinisicSuccess");
let expected_event = balances::events::Transfer {
from: alice.public_key().to_account_id(),
to: bob.public_key().to_account_id(),
amount: 10_000,
};
assert_eq!(event, expected_event);
let storage_at_post = api.storage().at_latest().await?;
let account_entry_post = storage_at_post.entry(&account_addr)?;
let alice_post = account_entry_post
.fetch((alice.public_key().to_account_id(),))
.await?
.decode()?;
let bob_post = account_entry_post
.fetch((bob.public_key().to_account_id(),))
.await?
.decode()?;
assert!(alice_pre.data.free - 10_000 >= alice_post.data.free);
assert_eq!(bob_pre.data.free + 10_000, bob_post.data.free);
Ok(())
}
#[cfg(fullclient)]
#[subxt_test]
async fn tx_dynamic_transfer() -> Result<(), subxt::Error> {
use subxt::ext::scale_value::{At, Value};
let alice = dev::alice();
let bob = dev::bob();
let ctx = test_context().await;
let api = ctx.client();
let account_addr = subxt::dynamic::storage::<(Value,), Value>("System", "Account");
let storage_at_pre = api.storage().at_latest().await?;
let account_entry_pre = storage_at_pre.entry(&account_addr)?;
let alice_pre = account_entry_pre
.fetch((Value::from_bytes(alice.public_key().to_account_id()),))
.await?
.decode()?;
let bob_pre = account_entry_pre
.fetch((Value::from_bytes(bob.public_key().to_account_id()),))
.await?
.decode()?;
let tx = subxt::dynamic::tx(
"Balances",
"transfer_allow_death",
vec![
Value::unnamed_variant(
"Id",
vec![Value::from_bytes(bob.public_key().to_account_id())],
),
Value::u128(10_000u128),
],
);
let events = api
.tx()
.sign_and_submit_then_watch_default(&tx, &alice)
.await?
.wait_for_finalized_success()
.await?;
let actual_transfer_event = events
.iter()
.filter_map(|ev| ev.ok())
.find(|ev| ev.pallet_name() == "Balances" && ev.variant_name() == "Transfer")
.expect("Failed to find Transfer event")
.decode_as_fields::<DecodedTransferEvent>()
.expect("Failed to decode event fields");
#[derive(DecodeAsType, Debug, PartialEq)]
#[decode_as_type(crate_path = "::pezkuwi_subxt::ext::scale_decode")]
struct DecodedTransferEvent {
from: AccountId32,
to: AccountId32,
amount: u128,
}
let expected_transfer_event = DecodedTransferEvent {
from: alice.public_key().to_account_id(),
to: bob.public_key().to_account_id(),
amount: 10000,
};
assert_eq!(actual_transfer_event, expected_transfer_event);
let storage_at_post = api.storage().at_latest().await?;
let account_entry_post = storage_at_post.entry(&account_addr)?;
let alice_post = account_entry_post
.fetch((Value::from_bytes(alice.public_key().to_account_id()),))
.await?
.decode()?;
let bob_post = account_entry_post
.fetch((Value::from_bytes(bob.public_key().to_account_id()),))
.await?
.decode()?;
let alice_pre_free = alice_pre.at("data").at("free").unwrap().as_u128().unwrap();
let alice_post_free = alice_post.at("data").at("free").unwrap().as_u128().unwrap();
let bob_pre_free = bob_pre.at("data").at("free").unwrap().as_u128().unwrap();
let bob_post_free = bob_post.at("data").at("free").unwrap().as_u128().unwrap();
assert!(alice_pre_free - 10_000 >= alice_post_free);
assert_eq!(bob_pre_free + 10_000, bob_post_free);
Ok(())
}
#[subxt_test]
async fn multiple_sequential_transfers_work() -> Result<(), subxt::Error> {
let alice = dev::alice();
let bob = dev::bob();
let bob_address: MultiAddress<AccountId32, u32> = bob.public_key().into();
let ctx = test_context().await;
let api = ctx.client();
let bob_pre = api
.storage()
.at_latest()
.await?
.fetch(
node_runtime::storage().system().account(),
(bob.public_key().to_account_id(),),
)
.await?
.decode()?;
// Do a transfer several times. If this works, it indicates that the
// nonce is properly incremented each time.
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob_address.clone(), 10_000);
for _ in 0..3 {
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await?;
signed_extrinsic
.submit_and_watch()
.await?
.wait_for_finalized_success()
.await?;
}
let bob_post = api
.storage()
.at_latest()
.await?
.fetch(
node_runtime::storage().system().account(),
(bob.public_key().to_account_id(),),
)
.await?
.decode()?;
assert_eq!(bob_pre.data.free + 30_000, bob_post.data.free);
Ok(())
}
#[subxt_test]
async fn storage_total_issuance() {
let ctx = test_context().await;
let api = ctx.client();
let addr = node_runtime::storage().balances().total_issuance();
let total_issuance = api
.storage()
.at_latest()
.await
.unwrap()
.entry(addr)
.unwrap()
.fetch()
.await
.unwrap()
.decode()
.unwrap();
assert_ne!(total_issuance, 0);
}
#[subxt_test]
async fn storage_balance_lock() -> Result<(), subxt::Error> {
let bob: AccountId32 = dev::bob().public_key().into();
let ctx = test_context().await;
let api = ctx.client();
let holds_addr = node_runtime::storage().balances().holds();
let holds = api
.storage()
.at_latest()
.await?
.fetch(holds_addr, (bob,))
.await?
.decode()?
.0;
assert_eq!(holds.len(), 0);
Ok(())
}
#[subxt_test]
async fn transfer_error() {
let alice = dev::alice();
let alice_addr = alice.public_key().into();
let bob = dev::one(); // some dev account with no funds.
let bob_address = bob.public_key().into();
let ctx = test_context().await;
let api = ctx.client();
let to_bob_tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob_address, 100_000_000_000_000_000);
let to_alice_tx = node_runtime::tx()
.balances()
.transfer_allow_death(alice_addr, 100_000_000_000_000_000);
let signed_extrinsic = api
.tx()
.create_signed(&to_bob_tx, &alice, Default::default())
.await
.unwrap();
signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await
.unwrap();
// When we try giving all of the funds back, Bob doesn't have
// anything left to pay transfer fees, so we hit an error.
let signed_extrinsic = api
.tx()
.create_signed(&to_alice_tx, &bob, Default::default())
.await
.unwrap();
let res = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await;
// Check that we get a FundsUnavailable error
let is_funds_unavailable = matches!(
res,
Err(TransactionFinalizedSuccessError::SuccessError(
TransactionEventsError::ExtrinsicFailed(DispatchError::Token(
TokenError::FundsUnavailable
)),
))
);
assert!(
is_funds_unavailable,
"Expected an insufficient balance, got {res:?}"
);
}
#[subxt_test]
async fn transfer_implicit_subscription() {
let alice = dev::alice();
let bob: AccountId32 = dev::bob().public_key().into();
let ctx = test_context().await;
let api = ctx.client();
let to_bob_tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob.clone().into(), 10_000);
let signed_extrinsic = api
.tx()
.create_signed(&to_bob_tx, &alice, Default::default())
.await
.unwrap();
let event = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await
.unwrap()
.find_first::<balances::events::Transfer>()
.expect("Can decode events")
.expect("Can find balance transfer event");
assert_eq!(
event,
balances::events::Transfer {
from: alice.public_key().to_account_id(),
to: bob,
amount: 10_000
}
);
}
#[subxt_test]
async fn constant_existential_deposit() {
let ctx = test_context().await;
let api = ctx.client();
// get and decode constant manually via metadata:
let metadata = api.metadata();
let balances_metadata = metadata.pallet_by_name("Balances").unwrap();
let constant_metadata = balances_metadata
.constant_by_name("ExistentialDeposit")
.unwrap();
let existential_deposit = u128::decode(&mut constant_metadata.value()).unwrap();
assert_eq!(existential_deposit, 100_000_000_000_000);
// constant address for API access:
let addr = node_runtime::constants().balances().existential_deposit();
// Make sure thetwo are identical:
assert_eq!(existential_deposit, api.constants().at(&addr).unwrap());
}
@@ -0,0 +1,249 @@
// 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::{
TestClient, TestConfig, TestContext,
node_runtime::{
self,
contracts::events,
runtime_types::{pallet_contracts::wasm::Determinism, sp_weights::weight_v2::Weight},
system,
},
subxt_test, test_context,
};
use subxt::ext::futures::StreamExt;
use subxt::{
Error,
config::{Config, HashFor},
tx::TxProgress,
utils::MultiAddress,
};
use pezkuwi_subxt_signer::sr25519::{self, dev};
struct ContractsTestContext {
cxt: TestContext,
signer: sr25519::Keypair,
}
type Hash = HashFor<TestConfig>;
type AccountId = <TestConfig as Config>::AccountId;
/// A dummy contract which does nothing at all.
const CONTRACT: &str = r#"
(module
(import "env" "memory" (memory 1 1))
(func (export "deploy"))
(func (export "call"))
)
"#;
const PROOF_SIZE: u64 = u64::MAX / 2;
impl ContractsTestContext {
async fn init() -> Self {
let cxt = test_context().await;
let signer = dev::alice();
Self { cxt, signer }
}
fn client(&self) -> TestClient {
self.cxt.client()
}
async fn upload_code(&self) -> Result<Hash, Error> {
let code = wat::parse_str(CONTRACT).expect("invalid wat");
let upload_tx =
node_runtime::tx()
.contracts()
.upload_code(code, None, Determinism::Enforced);
let signed_extrinsic = self
.client()
.tx()
.create_signed(&upload_tx, &self.signer, Default::default())
.await?;
let events = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await?;
let code_stored = events
.find_first::<events::CodeStored>()?
.ok_or_else(|| Error::other_str("Failed to find a CodeStored event"))?;
Ok(code_stored.code_hash)
}
async fn instantiate_with_code(&self) -> Result<(Hash, AccountId), Error> {
let code = wat::parse_str(CONTRACT).expect("invalid wat");
let instantiate_tx = node_runtime::tx().contracts().instantiate_with_code(
100_000_000_000_000_000, // endowment
Weight {
ref_time: 500_000_000_000,
proof_size: PROOF_SIZE,
}, // gas_limit
None, // storage_deposit_limit
code,
vec![], // data
vec![], // salt
);
let signed_extrinsic = self
.client()
.tx()
.create_signed(&instantiate_tx, &self.signer, Default::default())
.await?;
let events = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await?;
let code_stored = events
.find_first::<events::CodeStored>()?
.ok_or_else(|| Error::other_str("Failed to find a CodeStored event"))?;
let instantiated = events
.find_first::<events::Instantiated>()?
.ok_or_else(|| Error::other_str("Failed to find a Instantiated event"))?;
let _extrinsic_success = events
.find_first::<system::events::ExtrinsicSuccess>()?
.ok_or_else(|| Error::other_str("Failed to find a ExtrinsicSuccess event"))?;
tracing::info!(" Code hash: {:?}", code_stored.code_hash);
tracing::info!(" Contract address: {:?}", instantiated.contract);
Ok((code_stored.code_hash, instantiated.contract))
}
async fn instantiate(
&self,
code_hash: Hash,
data: Vec<u8>,
salt: Vec<u8>,
) -> Result<AccountId, Error> {
// call instantiate extrinsic
let instantiate_tx = node_runtime::tx().contracts().instantiate(
100_000_000_000_000_000, // endowment
Weight {
ref_time: 500_000_000_000,
proof_size: PROOF_SIZE,
}, // gas_limit
None, // storage_deposit_limit
code_hash,
data,
salt,
);
let signed_extrinsic = self
.client()
.tx()
.create_signed(&instantiate_tx, &self.signer, Default::default())
.await?;
let result = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await?;
tracing::info!("Instantiate result: {:?}", result);
let instantiated = result
.find_first::<events::Instantiated>()?
.ok_or_else(|| Error::other_str("Failed to find a Instantiated event"))?;
Ok(instantiated.contract)
}
async fn call(
&self,
contract: AccountId,
input_data: Vec<u8>,
) -> Result<TxProgress<TestConfig, TestClient>, Error> {
tracing::info!("call: {:?}", contract);
let call_tx = node_runtime::tx().contracts().call(
MultiAddress::Id(contract),
0, // value
Weight {
ref_time: 500_000_000,
proof_size: PROOF_SIZE,
}, // gas_limit
None, // storage_deposit_limit
input_data,
);
let result = self
.client()
.tx()
.sign_and_submit_then_watch_default(&call_tx, &self.signer)
.await?;
tracing::info!("Call result: {:?}", result);
Ok(result)
}
}
#[subxt_test]
async fn tx_instantiate_with_code() {
let ctx = ContractsTestContext::init().await;
let result = ctx.instantiate_with_code().await;
assert!(
result.is_ok(),
"Error calling instantiate_with_code and receiving CodeStored and Instantiated Events: {result:?}"
);
}
#[subxt_test]
async fn tx_instantiate() {
let ctx = ContractsTestContext::init().await;
let code_hash = ctx.upload_code().await.unwrap();
let instantiated = ctx.instantiate(code_hash, vec![], vec![]).await;
assert!(
instantiated.is_ok(),
"Error instantiating contract: {instantiated:?}"
);
}
#[subxt_test]
async fn tx_call() {
let cxt = ContractsTestContext::init().await;
let (_, contract) = cxt.instantiate_with_code().await.unwrap();
let storage_at = cxt.client().storage().at_latest().await.unwrap();
let contract_info_addr = node_runtime::storage().contracts().contract_info_of();
let contract_info = storage_at
.fetch(&contract_info_addr, (contract.clone(),))
.await
.unwrap();
assert!(
contract_info.decode().is_ok(),
"Contract info is not ok, is: {contract_info:?}"
);
let mut iter = storage_at.iter(contract_info_addr, ()).await.unwrap();
let mut keys_and_values = Vec::new();
while let Some(kv) = iter.next().await {
keys_and_values.push(kv);
}
assert_eq!(keys_and_values.len(), 1);
println!("keys+values post: {keys_and_values:?}");
let executed = cxt.call(contract, vec![]).await;
assert!(executed.is_ok(), "Error calling contract: {executed:?}");
}
@@ -0,0 +1,15 @@
// 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.
//! Test interactions with some built-in FRAME pallets.
mod balances;
mod staking;
mod system;
mod timestamp;
#[cfg(fullclient)]
mod contracts;
#[cfg(fullclient)]
mod sudo;
@@ -0,0 +1,326 @@
// 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::{
node_runtime::{
self,
runtime_types::{
pallet_staking::{RewardDestination, ValidatorPrefs},
sp_arithmetic::per_things::Perbill,
},
staking,
},
subxt_test, test_context,
};
use subxt::error::{
DispatchError, Error, TransactionEventsError, TransactionFinalizedSuccessError,
};
use pezkuwi_subxt_signer::{
SecretUri,
sr25519::{self, dev},
};
/// Helper function to generate a crypto pair from seed
fn get_from_seed(seed: &str) -> sr25519::Keypair {
use std::str::FromStr;
let uri = SecretUri::from_str(&format!("//{seed}")).expect("expected to be valid");
sr25519::Keypair::from_uri(&uri).expect("expected to be valid")
}
fn default_validator_prefs() -> ValidatorPrefs {
ValidatorPrefs {
commission: Perbill(0),
blocked: false,
}
}
#[subxt_test]
async fn validate_with_stash_account() {
let ctx = test_context().await;
let api = ctx.client();
let alice_stash = get_from_seed("Alice//stash");
let tx = node_runtime::tx()
.staking()
.validate(default_validator_prefs());
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice_stash, Default::default())
.await
.unwrap();
signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await
.expect("should be successful");
}
#[subxt_test]
async fn validate_not_possible_for_controller_account() -> Result<(), Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let tx = node_runtime::tx()
.staking()
.validate(default_validator_prefs());
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await?;
let announce_validator = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await;
if let Err(TransactionFinalizedSuccessError::SuccessError(
TransactionEventsError::ExtrinsicFailed(DispatchError::Module(err)),
)) = announce_validator
{
let details = err.details().unwrap();
assert_eq!(details.pallet.name(), "Staking");
assert_eq!(&details.variant.name, "NotController");
} else {
panic!("Expected an error");
}
Ok(())
}
#[subxt_test]
async fn nominate_with_stash_account() {
let ctx = test_context().await;
let api = ctx.client();
let alice_stash = get_from_seed("Alice//stash");
let bob = dev::bob();
let tx = node_runtime::tx()
.staking()
.nominate(vec![bob.public_key().to_address()]);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice_stash, Default::default())
.await
.unwrap();
signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await
.expect("should be successful");
}
#[subxt_test]
async fn nominate_not_possible_for_controller_account() -> Result<(), Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let bob = dev::bob();
let tx = node_runtime::tx()
.staking()
.nominate(vec![bob.public_key().to_address()]);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await
.unwrap();
let nomination = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await;
if let Err(TransactionFinalizedSuccessError::SuccessError(
TransactionEventsError::ExtrinsicFailed(DispatchError::Module(err)),
)) = nomination
{
let details = err.details().unwrap();
assert_eq!(details.pallet.name(), "Staking");
assert_eq!(&details.variant.name, "NotController");
} else {
panic!("Expected an error");
}
Ok(())
}
#[subxt_test]
async fn chill_works_for_stash_only() -> Result<(), Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice_stash = get_from_seed("Alice//stash");
let bob_stash = get_from_seed("Bob//stash");
let alice = dev::alice();
// this will fail the second time, which is why this is one test, not two
let nominate_tx = node_runtime::tx()
.staking()
.nominate(vec![bob_stash.public_key().to_address()]);
let signed_extrinsic = api
.tx()
.create_signed(&nominate_tx, &alice_stash, Default::default())
.await?;
signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await?;
let ledger_addr = node_runtime::storage().staking().ledger();
let ledger = api
.storage()
.at_latest()
.await?
.fetch(ledger_addr, (alice_stash.public_key().to_account_id(),))
.await?
.decode()?;
assert_eq!(alice_stash.public_key().to_account_id(), ledger.stash);
let chill_tx = node_runtime::tx().staking().chill();
let signed_extrinsic = api
.tx()
.create_signed(&chill_tx, &alice, Default::default())
.await?;
let chill = signed_extrinsic
.submit_and_watch()
.await
.unwrap()
.wait_for_finalized_success()
.await;
if let Err(TransactionFinalizedSuccessError::SuccessError(
TransactionEventsError::ExtrinsicFailed(DispatchError::Module(err)),
)) = chill
{
let details = err.details().unwrap();
assert_eq!(details.pallet.name(), "Staking");
assert_eq!(&details.variant.name, "NotController");
} else {
panic!("Expected an error");
}
let signed_extrinsic = api
.tx()
.create_signed(&chill_tx, &alice_stash, Default::default())
.await?;
let is_chilled = signed_extrinsic
.submit_and_watch()
.await?
.wait_for_finalized_success()
.await?
.has::<staking::events::Chilled>()?;
assert!(is_chilled);
Ok(())
}
#[subxt_test]
async fn tx_bond() -> Result<(), Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let bond_tx = node_runtime::tx()
.staking()
.bond(100_000_000_000_000, RewardDestination::Stash);
let signed_extrinsic = api
.tx()
.create_signed(&bond_tx, &alice, Default::default())
.await?;
let bond = signed_extrinsic
.submit_and_watch()
.await?
.wait_for_finalized_success()
.await;
assert!(bond.is_ok());
let signed_extrinsic = api
.tx()
.create_signed(&bond_tx, &alice, Default::default())
.await?;
let bond_again = signed_extrinsic
.submit_and_watch()
.await?
.wait_for_finalized_success()
.await;
if let Err(TransactionFinalizedSuccessError::SuccessError(
TransactionEventsError::ExtrinsicFailed(DispatchError::Module(err)),
)) = bond_again
{
let details = err.details().unwrap();
assert_eq!(details.pallet.name(), "Staking");
assert_eq!(&details.variant.name, "AlreadyBonded");
} else {
panic!("Expected an error");
}
Ok(())
}
#[subxt_test]
async fn storage_history_depth() -> Result<(), Error> {
let ctx = test_context().await;
let api = ctx.client();
let history_depth_addr = node_runtime::constants().staking().history_depth();
let history_depth = api.constants().at(&history_depth_addr)?;
assert_eq!(history_depth, 84);
Ok(())
}
#[subxt_test]
async fn storage_current_era() -> Result<(), Error> {
let ctx = test_context().await;
let api = ctx.client();
let current_era_addr = node_runtime::storage().staking().current_era();
let _current_era = api
.storage()
.at_latest()
.await?
.fetch(current_era_addr, ())
.await?
.decode()?;
Ok(())
}
#[subxt_test]
async fn storage_era_reward_points() -> Result<(), Error> {
let ctx = test_context().await;
let api = ctx.client();
let reward_points_addr = node_runtime::storage().staking().eras_reward_points();
let current_era_result = api
.storage()
.at_latest()
.await?
.fetch(reward_points_addr, (0,))
.await?
.decode();
assert!(current_era_result.is_ok());
Ok(())
}
@@ -0,0 +1,82 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{
node_runtime::{
self,
runtime_types::{self, sp_weights::weight_v2::Weight},
sudo,
},
subxt_test, test_context,
};
use pezkuwi_subxt_signer::sr25519::dev;
type Call = runtime_types::kitchensink_runtime::RuntimeCall;
type BalancesCall = runtime_types::pallet_balances::pallet::Call;
#[subxt_test]
async fn test_sudo() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let bob = dev::bob().public_key().into();
let call = Call::Balances(BalancesCall::transfer_allow_death {
dest: bob,
value: 10_000,
});
let tx = node_runtime::tx().sudo().sudo(call);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await?;
let found_event = signed_extrinsic
.submit_and_watch()
.await?
.wait_for_finalized_success()
.await?
.has::<sudo::events::Sudid>()?;
assert!(found_event);
Ok(())
}
#[subxt_test]
async fn test_sudo_unchecked_weight() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let bob = dev::bob().public_key().into();
let call = Call::Balances(BalancesCall::transfer_allow_death {
dest: bob,
value: 10_000,
});
let tx = node_runtime::tx().sudo().sudo_unchecked_weight(
call,
Weight {
ref_time: 0,
proof_size: 0,
},
);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await?;
let found_event = signed_extrinsic
.submit_and_watch()
.await?
.wait_for_finalized_success()
.await?
.has::<sudo::events::Sudid>()?;
assert!(found_event);
Ok(())
}
@@ -0,0 +1,56 @@
// 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::{
node_runtime::{self, system},
subxt_test, test_context,
};
use pezkuwi_subxt_signer::sr25519::dev;
#[subxt_test]
async fn storage_account() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let account_info_addr = node_runtime::storage().system().account();
let _account_info = api
.storage()
.at_latest()
.await?
.fetch(account_info_addr, (alice.public_key().to_account_id(),))
.await?
.decode()?;
Ok(())
}
#[subxt_test]
async fn tx_remark_with_event() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let tx = node_runtime::tx()
.system()
.remark_with_event(b"remarkable".to_vec());
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await?;
let found_event = signed_extrinsic
.submit_and_watch()
.await?
.wait_for_finalized_success()
.await?
.has::<system::events::Remarked>()?;
assert!(found_event);
Ok(())
}
@@ -0,0 +1,22 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{node_runtime, subxt_test, test_context};
#[subxt_test]
async fn storage_get_current_timestamp() {
let ctx = test_context().await;
let api = ctx.client();
let storage_at = api.storage().at_latest().await.unwrap();
let timestamp_value = storage_at
.entry(node_runtime::storage().timestamp().now())
.unwrap()
.fetch()
.await
.unwrap();
assert!(timestamp_value.decode().is_ok())
}
@@ -0,0 +1,351 @@
// 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::{TestContext, node_runtime, subxt_test, test_context};
use codec::Decode;
use frame_metadata::{
RuntimeMetadata, RuntimeMetadataPrefixed,
v15::{
CustomMetadata, ExtrinsicMetadata, OuterEnums, PalletCallMetadata, PalletMetadata,
PalletStorageMetadata, RuntimeMetadataV15, StorageEntryMetadata, StorageEntryModifier,
StorageEntryType,
},
};
use scale_info::{
Path, Type, TypeInfo,
build::{Fields, Variants},
meta_type,
};
use subxt::{Metadata, OfflineClient, OnlineClient, SubstrateConfig};
async fn fetch_v15_metadata(client: &OnlineClient<SubstrateConfig>) -> RuntimeMetadataV15 {
let payload = node_runtime::apis().metadata().metadata_at_version(15);
let runtime_metadata_bytes = client
.runtime_api()
.at_latest()
.await
.unwrap()
.call(payload)
.await
.unwrap()
.unwrap()
.0;
let runtime_metadata = RuntimeMetadataPrefixed::decode(&mut &*runtime_metadata_bytes)
.unwrap()
.1;
let RuntimeMetadata::V15(v15_metadata) = runtime_metadata else {
panic!("Metadata is not v15")
};
v15_metadata
}
async fn metadata_to_api(metadata: Metadata, ctx: &TestContext) -> OfflineClient<SubstrateConfig> {
OfflineClient::new(
ctx.client().genesis_hash(),
ctx.client().runtime_version(),
metadata,
)
}
fn v15_to_metadata(v15: RuntimeMetadataV15) -> Metadata {
let subxt_md: pezkuwi_subxt_metadata::Metadata = v15.try_into().unwrap();
subxt_md
}
fn default_pallet() -> PalletMetadata {
PalletMetadata {
name: "Test",
storage: None,
calls: None,
event: None,
constants: vec![],
error: None,
index: 0,
docs: vec![],
}
}
fn pallets_to_metadata(pallets: Vec<PalletMetadata>) -> Metadata {
// Extrinsic needs to contain at least the generic type parameter "Call"
// for the metadata to be valid.
// The "Call" type from the metadata is used to decode extrinsics.
// In reality, the extrinsic type has "Call", "Address", "Extra", "Signature" generic types.
#[allow(unused)]
#[derive(TypeInfo)]
struct ExtrinsicType<Call> {
call: Call,
}
// Because this type is used to decode extrinsics, we expect this to be a TypeDefVariant.
// Each pallet must contain one single variant.
#[allow(unused)]
#[derive(TypeInfo)]
enum RuntimeCall {
PalletName(Pallet),
}
// The calls of the pallet.
#[allow(unused)]
#[derive(TypeInfo)]
enum Pallet {
#[allow(unused)]
SomeCall,
}
v15_to_metadata(RuntimeMetadataV15::new(
pallets,
ExtrinsicMetadata {
version: 0,
signed_extensions: vec![],
address_ty: meta_type::<()>(),
call_ty: meta_type::<RuntimeCall>(),
signature_ty: meta_type::<()>(),
extra_ty: meta_type::<()>(),
},
meta_type::<()>(),
vec![],
OuterEnums {
call_enum_ty: meta_type::<()>(),
event_enum_ty: meta_type::<()>(),
error_enum_ty: meta_type::<()>(),
},
CustomMetadata {
map: Default::default(),
},
))
}
#[subxt_test]
async fn full_metadata_check() {
let ctx = test_context().await;
let api = ctx.client();
let mut v15_metadata = fetch_v15_metadata(&api).await;
// Runtime metadata is identical to the metadata we just downloaded
let metadata_before = v15_to_metadata(v15_metadata.clone());
assert!(node_runtime::is_codegen_valid_for(&metadata_before));
// Modify the metadata.
v15_metadata.pallets[0].name = "NewPallet".to_string();
// It should now be invalid:
let metadata_after = v15_to_metadata(v15_metadata);
assert!(!node_runtime::is_codegen_valid_for(&metadata_after));
}
#[subxt_test]
async fn constant_values_are_not_validated() {
let ctx = test_context().await;
let api = ctx.client();
let mut v15_metadata = fetch_v15_metadata(&api).await;
// Build an api from our v15 metadata to confirm that it's good, just like
// the metadata downloaded by the API itself.
let api_from_original_metadata = {
let metadata_before = v15_to_metadata(v15_metadata.clone());
metadata_to_api(metadata_before, &ctx).await
};
let deposit_addr = node_runtime::constants().balances().existential_deposit();
// Retrieve existential deposit to validate it and confirm that it's OK.
assert!(
api_from_original_metadata
.constants()
.at(&deposit_addr)
.is_ok()
);
// Modify the metadata.
let existential = v15_metadata
.pallets
.iter_mut()
.find(|pallet| pallet.name == "Balances")
.expect("Metadata must contain Balances pallet")
.constants
.iter_mut()
.find(|constant| constant.name == "ExistentialDeposit")
.expect("ExistentialDeposit constant must be present");
// Modifying a constant value should not lead to an error:
existential.value = vec![0u8; 16];
// Build our API again, this time from the metadata we've tweaked.
let api_from_modified_metadata = {
let metadata_before = v15_to_metadata(v15_metadata);
metadata_to_api(metadata_before, &ctx).await
};
assert!(node_runtime::is_codegen_valid_for(
&api_from_modified_metadata.metadata()
));
assert!(
api_from_modified_metadata
.constants()
.at(&deposit_addr)
.is_ok()
);
}
#[subxt_test]
async fn calls_check() {
let ctx = test_context().await;
let api = ctx.client();
let unbond_tx = node_runtime::tx().staking().unbond(123_456_789_012_345);
let withdraw_unbonded_addr = node_runtime::tx().staking().withdraw_unbonded(10);
// Ensure that `Unbond` and `WinthdrawUnbonded` calls are compatible before altering the metadata.
assert!(api.tx().validate(&unbond_tx).is_ok());
assert!(api.tx().validate(&withdraw_unbonded_addr).is_ok());
// Reconstruct the `Staking` call as is.
struct CallRec;
impl TypeInfo for CallRec {
type Identity = Self;
fn type_info() -> Type {
Type::builder()
.path(Path::new("Call", "pallet_staking::pallet::pallet"))
.variant(
Variants::new()
.variant("unbond", |v| {
v.index(0).fields(Fields::named().field(|f| {
f.compact::<u128>().name("value").type_name("BalanceOf<T>")
}))
})
.variant("withdraw_unbonded", |v| {
v.index(1).fields(Fields::named().field(|f| {
f.ty::<u32>().name("num_slashing_spans").type_name("u32")
}))
}),
)
}
}
let pallet = PalletMetadata {
name: "Staking",
calls: Some(PalletCallMetadata {
ty: meta_type::<CallRec>(),
}),
..default_pallet()
};
let metadata = pallets_to_metadata(vec![pallet]);
let api = metadata_to_api(metadata, &ctx).await;
// The calls should still be valid with this new type info:
assert!(api.tx().validate(&unbond_tx).is_ok());
assert!(api.tx().validate(&withdraw_unbonded_addr).is_ok());
// Change `Unbond` call but leave the rest as is.
struct CallRecSecond;
impl TypeInfo for CallRecSecond {
type Identity = Self;
fn type_info() -> Type {
Type::builder()
.path(Path::new("Call", "pallet_staking::pallet::pallet"))
.variant(
Variants::new()
.variant("unbond", |v| {
v.index(0).fields(Fields::named().field(|f| {
// Is of type u32 instead of u128.
f.compact::<u32>().name("value").type_name("BalanceOf<T>")
}))
})
.variant("withdraw_unbonded", |v| {
v.index(1).fields(Fields::named().field(|f| {
f.ty::<u32>().name("num_slashing_spans").type_name("u32")
}))
}),
)
}
}
let pallet = PalletMetadata {
name: "Staking",
calls: Some(PalletCallMetadata {
ty: meta_type::<CallRecSecond>(),
}),
..default_pallet()
};
let metadata = pallets_to_metadata(vec![pallet]);
let api = metadata_to_api(metadata, &ctx).await;
// Unbond call should fail, while withdraw_unbonded remains compatible.
assert!(api.tx().validate(&unbond_tx).is_err());
assert!(api.tx().validate(&withdraw_unbonded_addr).is_ok());
}
#[subxt_test]
async fn storage_check() {
let ctx = test_context().await;
let api = ctx.client();
let tx_count_addr = node_runtime::storage().system().extrinsic_count();
let tx_len_addr = node_runtime::storage().system().all_extrinsics_len();
// Ensure that `ExtrinsicCount` and `EventCount` storages are compatible before altering the metadata.
assert!(api.storage().validate(&tx_count_addr).is_ok());
assert!(api.storage().validate(&tx_len_addr).is_ok());
// Reconstruct the storage.
let storage = PalletStorageMetadata {
prefix: "System",
entries: vec![
StorageEntryMetadata {
name: "ExtrinsicCount",
modifier: StorageEntryModifier::Optional,
ty: StorageEntryType::Plain(meta_type::<u32>()),
default: vec![0],
docs: vec![],
},
StorageEntryMetadata {
name: "AllExtrinsicsLen",
modifier: StorageEntryModifier::Optional,
ty: StorageEntryType::Plain(meta_type::<u32>()),
default: vec![0],
docs: vec![],
},
],
};
let pallet = PalletMetadata {
name: "System",
storage: Some(storage),
..default_pallet()
};
let metadata = pallets_to_metadata(vec![pallet]);
let api = metadata_to_api(metadata, &ctx).await;
// The addresses should still validate:
assert!(api.storage().validate(&tx_count_addr).is_ok());
assert!(api.storage().validate(&tx_len_addr).is_ok());
// Reconstruct the storage while modifying ExtrinsicCount.
let storage = PalletStorageMetadata {
prefix: "System",
entries: vec![
StorageEntryMetadata {
name: "ExtrinsicCount",
modifier: StorageEntryModifier::Optional,
// Previously was u32.
ty: StorageEntryType::Plain(meta_type::<u8>()),
default: vec![0],
docs: vec![],
},
StorageEntryMetadata {
name: "AllExtrinsicsLen",
modifier: StorageEntryModifier::Optional,
ty: StorageEntryType::Plain(meta_type::<u32>()),
default: vec![0],
docs: vec![],
},
],
};
let pallet = PalletMetadata {
name: "System",
storage: Some(storage),
..default_pallet()
};
let metadata = pallets_to_metadata(vec![pallet]);
let api = metadata_to_api(metadata, &ctx).await;
// The count route should fail now; the other will be ok still.
assert!(api.storage().validate(&tx_count_addr).is_err());
assert!(api.storage().validate(&tx_len_addr).is_ok());
}
@@ -0,0 +1,13 @@
// 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.
mod blocks;
mod client;
mod codegen;
mod frame;
mod metadata_validation;
mod pallet_view_functions;
mod runtime_api;
mod storage;
mod transactions;
@@ -0,0 +1,68 @@
// 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.
// TODO: Re-enable these once V16 is stable in Substrate nightlies,
// and test-runtime is updated to pull in V16 metadata by default.
/*
use crate::{subxt_test, test_context};
use test_runtime::node_runtime_unstable;
#[subxt_test]
async fn call_view_function() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
use node_runtime_unstable::proxy::view_functions::check_permissions::{Call, ProxyType};
// This is one of only two view functions that currently exists at the time of writing.
let call = Call::System(node_runtime_unstable::system::Call::remark {
remark: b"hi".to_vec(),
});
let proxy_type = ProxyType::Any;
let view_function_call = node_runtime_unstable::view_functions()
.proxy()
.check_permissions(call, proxy_type);
// Submit the call and get back a result.
let _is_call_allowed = api
.view_functions()
.at_latest()
.await?
.call(view_function_call)
.await?;
Ok(())
}
#[subxt_test]
async fn call_view_function_dynamically() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let metadata = api.metadata();
let query_id = metadata
.pallet_by_name("Proxy")
.unwrap()
.view_function_by_name("check_permissions")
.unwrap()
.query_id();
use scale_value::value;
let view_function_call = subxt::dynamic::view_function_call(
*query_id,
vec![value!(System(remark(b"hi".to_vec()))), value!(Any())],
);
// Submit the call and get back a result.
let _is_call_allowed = api
.view_functions()
.at_latest()
.await?
.call(view_function_call)
.await?;
Ok(())
}
*/
@@ -0,0 +1,128 @@
// 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::{node_runtime, subxt_test, test_context};
use codec::{Decode, Encode};
use subxt::utils::AccountId32;
use pezkuwi_subxt_signer::sr25519::dev;
#[subxt_test]
async fn account_nonce() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let alice_account_id: AccountId32 = alice.public_key().into();
// Check Alice nonce is starting from 0.
let runtime_api_call = node_runtime::apis()
.account_nonce_api()
.account_nonce(alice_account_id.clone());
let nonce = api
.runtime_api()
.at_latest()
.await?
.call(runtime_api_call)
.await?;
assert_eq!(nonce, 0);
// Do some transaction to bump the Alice nonce to 1:
let remark_tx = node_runtime::tx().system().remark(vec![1, 2, 3, 4, 5]);
let signed_extrinsic = api
.tx()
.create_signed(&remark_tx, &alice, Default::default())
.await?;
signed_extrinsic
.submit_and_watch()
.await?
.wait_for_finalized_success()
.await?;
let runtime_api_call = node_runtime::apis()
.account_nonce_api()
.account_nonce(alice_account_id);
let nonce = api
.runtime_api()
.at_latest()
.await?
.call(runtime_api_call)
.await?;
assert_eq!(nonce, 1);
Ok(())
}
#[subxt_test]
async fn unchecked_extrinsic_encoding() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let alice = dev::alice();
let bob = dev::bob();
let bob_address = bob.public_key().to_address();
// Construct a tx from Alice to Bob.
let tx = node_runtime::tx()
.balances()
.transfer_allow_death(bob_address, 10_000);
let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await
.unwrap();
let tx_bytes = signed_extrinsic.into_encoded();
let len = tx_bytes.len() as u32;
// Manually encode the runtime API call arguments to make a raw call.
let mut encoded = tx_bytes.clone();
encoded.extend(len.encode());
// Use the raw API to manually build an expected result.
let expected_result = {
let expected_result_bytes = api
.runtime_api()
.at_latest()
.await?
.call_raw(
"TransactionPaymentApi_query_fee_details",
Some(encoded.as_ref()),
)
.await?;
// manually decode, since our runtime types don't impl Decode by default.
let (inclusion_fee, tip): (Option<(u128, u128, u128)>, u128) =
Decode::decode(&mut &*expected_result_bytes)?;
// put the values into our generated type.
node_runtime::runtime_types::pallet_transaction_payment::types::FeeDetails {
inclusion_fee: inclusion_fee.map(|(base_fee, len_fee, adjusted_weight_fee)| {
node_runtime::runtime_types::pallet_transaction_payment::types::InclusionFee {
base_fee,
len_fee,
adjusted_weight_fee,
}
}),
tip,
}
};
// Use the generated API to confirm the result with the raw call.
let runtime_api_call = node_runtime::apis()
.transaction_payment_api()
.query_fee_details(tx_bytes.into(), len);
let result = api
.runtime_api()
.at_latest()
.await?
.call(runtime_api_call)
.await?;
assert_eq!(expected_result, result);
Ok(())
}
@@ -0,0 +1,291 @@
// 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::{node_runtime, subxt_test, test_context, utils::wait_for_blocks};
use futures::StreamExt;
#[cfg(fullclient)]
use subxt::utils::AccountId32;
#[cfg(fullclient)]
use pezkuwi_subxt_signer::sr25519::dev;
#[subxt_test]
async fn storage_plain_lookup() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
// Look up a plain value. Wait long enough that we don't get the genesis block data,
// because it may have no storage associated with it.
wait_for_blocks(&api).await;
let addr = node_runtime::storage().timestamp().now();
let entry = api
.storage()
.at_latest()
.await?
.fetch(addr, ())
.await?
.decode()?;
assert!(entry > 0);
Ok(())
}
#[cfg(fullclient)]
#[subxt_test]
async fn storage_map_lookup() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let signer = dev::alice();
let alice: AccountId32 = dev::alice().public_key().into();
// Do some transaction to bump the Alice nonce to 1:
let remark_tx = node_runtime::tx().system().remark(vec![1, 2, 3, 4, 5]);
api.tx()
.sign_and_submit_then_watch_default(&remark_tx, &signer)
.await?
.wait_for_finalized_success()
.await?;
// Look up the nonce for the user (we expect it to be 1).
let nonce_addr = node_runtime::storage().system().account();
let entry = api
.storage()
.at_latest()
.await?
.fetch(nonce_addr, (alice,))
.await?
.decode()?;
assert_eq!(entry.nonce, 1);
Ok(())
}
#[cfg(fullclient)]
#[subxt_test]
async fn storage_n_mapish_key_is_properly_created() -> Result<(), subxt::Error> {
use codec::Encode;
use node_runtime::runtime_types::sp_core::crypto::KeyTypeId;
let ctx = test_context().await;
let api = ctx.client();
// This is what the generated code hashes a `session().key_owner(..)` key into:
let storage_addr = node_runtime::storage().session().key_owner();
let actual_key_bytes = api
.storage()
.at_latest()
.await?
.entry(storage_addr)?
.key(((KeyTypeId([1, 2, 3, 4]), vec![5, 6, 7, 8]),))?;
// Let's manually hash to what we assume it should be and compare:
let expected_key_bytes = {
// Hash the prefix to the storage entry:
let mut bytes = sp_core::twox_128("Session".as_bytes()).to_vec();
bytes.extend(&sp_core::twox_128("KeyOwner".as_bytes())[..]);
// Key is a tuple of 2 args, so encode each arg and then hash the concatenation:
let mut key_bytes = vec![];
[1u8, 2, 3, 4].encode_to(&mut key_bytes);
vec![5u8, 6, 7, 8].encode_to(&mut key_bytes);
bytes.extend(sp_core::twox_64(&key_bytes));
bytes.extend(&key_bytes);
bytes
};
assert_eq!(actual_key_bytes, expected_key_bytes);
Ok(())
}
#[cfg(fullclient)]
#[subxt_test]
async fn storage_n_map_storage_lookup() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
// Boilerplate; we create a new asset class with ID 99, and then
// we "approveTransfer" of some of this asset class. This gives us an
// entry in the `Approvals` StorageNMap that we can try to look up.
let signer = dev::alice();
let alice: AccountId32 = dev::alice().public_key().into();
let bob: AccountId32 = dev::bob().public_key().into();
let tx1 = node_runtime::tx()
.assets()
.create(99, alice.clone().into(), 1);
let tx2 = node_runtime::tx()
.assets()
.approve_transfer(99, bob.clone().into(), 123);
api.tx()
.sign_and_submit_then_watch_default(&tx1, &signer)
.await?
.wait_for_finalized_success()
.await?;
api.tx()
.sign_and_submit_then_watch_default(&tx2, &signer)
.await?
.wait_for_finalized_success()
.await?;
// The actual test; look up this approval in storage:
let addr = node_runtime::storage().assets().approvals();
let entry = api
.storage()
.at_latest()
.await?
.fetch(addr, (99, alice, bob))
.await?
.decode()?;
assert_eq!(entry.amount, 123);
Ok(())
}
#[cfg(fullclient)]
#[subxt_test]
async fn storage_partial_lookup() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
// Boilerplate; we create a new asset class with ID 99, and then
// we "approveTransfer" of some of this asset class. This gives us an
// entry in the `Approvals` StorageNMap that we can try to look up.
let signer = dev::alice();
let alice: AccountId32 = dev::alice().public_key().into();
let bob: AccountId32 = dev::bob().public_key().into();
// Create two assets; one with ID 99 and one with ID 100.
let assets = [
(99, alice.clone(), bob.clone(), 123),
(100, bob.clone(), alice.clone(), 124),
];
for (asset_id, admin, delegate, amount) in assets.clone() {
let tx1 = node_runtime::tx()
.assets()
.create(asset_id, admin.into(), 1);
let tx2 = node_runtime::tx()
.assets()
.approve_transfer(asset_id, delegate.into(), amount);
api.tx()
.sign_and_submit_then_watch_default(&tx1, &signer)
.await?
.wait_for_finalized_success()
.await?;
api.tx()
.sign_and_submit_then_watch_default(&tx2, &signer)
.await?
.wait_for_finalized_success()
.await?;
}
// Check all approvals.
let approvals_addr = node_runtime::storage().assets().approvals();
let storage_at = api.storage().at_latest().await?;
let approvals_entry = storage_at.entry(approvals_addr)?;
let mut results = approvals_entry.iter(()).await?;
let mut approvals = Vec::new();
while let Some(kv) = results.next().await {
let kv = kv?;
assert!(kv.key_bytes().starts_with(&approvals_entry.key_prefix()));
approvals.push(kv.value().decode()?);
}
assert_eq!(approvals.len(), assets.len());
let mut amounts = approvals.iter().map(|a| a.amount).collect::<Vec<_>>();
amounts.sort();
let mut expected = assets.iter().map(|a| a.3).collect::<Vec<_>>();
expected.sort();
assert_eq!(amounts, expected);
// Check all assets starting with ID 99.
for (asset_id, _, _, amount) in assets.clone() {
let mut results = approvals_entry.iter((asset_id,)).await?;
let mut approvals = Vec::new();
while let Some(kv) = results.next().await {
let kv = kv?;
assert!(kv.key_bytes().starts_with(&approvals_entry.key_prefix()));
approvals.push(kv.value().decode()?);
}
assert_eq!(approvals.len(), 1);
assert_eq!(approvals[0].amount, amount);
}
Ok(())
}
#[subxt_test]
async fn storage_runtime_wasm_code() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let wasm_blob = api.storage().at_latest().await?.runtime_wasm_code().await?;
assert!(wasm_blob.len() > 1000); // the wasm should be super big
Ok(())
}
#[subxt_test]
async fn storage_pallet_storage_version() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
// cannot assume anything about version number, but should work to fetch it
let _version = api
.storage()
.at_latest()
.await?
.storage_version("System")
.await?;
let _version = api
.storage()
.at_latest()
.await?
.storage_version("Balances")
.await?;
Ok(())
}
#[subxt_test]
async fn storage_iter_decode_keys() -> Result<(), subxt::Error> {
use futures::StreamExt;
let ctx = test_context().await;
let api = ctx.client();
let storage_static = node_runtime::storage().system().account();
let storage_at_static = api.storage().at_latest().await?;
let results_static = storage_at_static.iter(storage_static, ()).await?;
let storage_dynamic =
subxt::dynamic::storage::<(scale_value::Value,), scale_value::Value>("System", "Account");
let storage_at_dynamic = api.storage().at_latest().await?;
let results_dynamic = storage_at_dynamic.iter(storage_dynamic, ()).await?;
// Even the testing node should have more than 3 accounts registered.
let results_static = results_static.take(3).collect::<Vec<_>>().await;
let results_dynamic = results_dynamic.take(3).collect::<Vec<_>>().await;
assert_eq!(results_static.len(), 3);
assert_eq!(results_dynamic.len(), 3);
let twox_system = sp_core::twox_128("System".as_bytes());
let twox_account = sp_core::twox_128("Account".as_bytes());
for (static_kv, dynamic_kv) in results_static.into_iter().zip(results_dynamic.into_iter()) {
let static_kv = static_kv?;
let dynamic_kv = dynamic_kv?;
// We only care about the underlying key bytes.
assert_eq!(static_kv.key_bytes(), dynamic_kv.key_bytes());
let bytes = static_kv.key_bytes();
assert!(bytes.len() > 32);
// The first 16 bytes should be the twox hash of "System" and the next 16 bytes should be the twox hash of "Account".
assert_eq!(&bytes[..16], &twox_system[..]);
assert_eq!(&bytes[16..32], &twox_account[..]);
}
Ok(())
}
@@ -0,0 +1,133 @@
// 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::utils::node_runtime;
use crate::{subxt_test, test_context};
use frame_decode::extrinsics::ExtrinsicType;
use pezkuwi_subxt_signer::sr25519::dev;
// TODO: When VerifySignature exists on the substrate kitchensink runtime,
// let's try actuallty submitting v4 and v5 signed extrinsics to verify that
// they are actually accepted by the node.
#[subxt_test]
async fn v4_unsigned_encode_decode() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let md = api.metadata();
let call = node_runtime::tx()
.balances()
.transfer_allow_death(dev::bob().public_key().into(), 1000);
let tx_bytes = api.tx().create_v4_unsigned(&call).unwrap().into_encoded();
let tx_bytes_cursor = &mut &*tx_bytes;
let decoded =
frame_decode::extrinsics::decode_extrinsic(tx_bytes_cursor, &md, api.metadata().types())
.unwrap();
assert_eq!(tx_bytes_cursor.len(), 0);
assert_eq!(decoded.version(), 4);
assert_eq!(decoded.ty(), ExtrinsicType::Bare);
assert_eq!(decoded.pallet_name(), "Balances");
assert_eq!(decoded.call_name(), "transfer_allow_death");
assert!(decoded.signature_payload().is_none());
Ok(())
}
#[subxt_test]
async fn v5_bare_encode_decode() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let md = api.metadata();
let call = node_runtime::tx()
.balances()
.transfer_allow_death(dev::bob().public_key().into(), 1000);
let tx_bytes = api.tx().create_v5_bare(&call).unwrap().into_encoded();
let tx_bytes_cursor = &mut &*tx_bytes;
let decoded =
frame_decode::extrinsics::decode_extrinsic(tx_bytes_cursor, &md, md.types()).unwrap();
assert_eq!(tx_bytes_cursor.len(), 0);
assert_eq!(decoded.version(), 5);
assert_eq!(decoded.ty(), ExtrinsicType::Bare);
assert_eq!(decoded.pallet_name(), "Balances");
assert_eq!(decoded.call_name(), "transfer_allow_death");
assert!(decoded.transaction_extension_payload().is_none());
assert!(decoded.signature_payload().is_none());
Ok(())
}
#[subxt_test]
async fn v4_signed_encode_decode() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let md = api.metadata();
let call = node_runtime::tx()
.balances()
.transfer_allow_death(dev::bob().public_key().into(), 1000);
let tx_bytes = api
.tx()
.create_v4_partial(&call, &dev::alice().public_key().into(), Default::default())
.await
.unwrap()
.sign(&dev::alice())
.into_encoded();
let tx_bytes_cursor = &mut &*tx_bytes;
let decoded =
frame_decode::extrinsics::decode_extrinsic(tx_bytes_cursor, &md, md.types()).unwrap();
assert_eq!(tx_bytes_cursor.len(), 0);
assert_eq!(decoded.version(), 4);
assert_eq!(decoded.ty(), ExtrinsicType::Signed);
assert_eq!(decoded.pallet_name(), "Balances");
assert_eq!(decoded.call_name(), "transfer_allow_death");
assert!(decoded.signature_payload().is_some());
Ok(())
}
#[subxt_test]
async fn v5_general_encode_decode() -> Result<(), subxt::Error> {
let ctx = test_context().await;
let api = ctx.client();
let md = api.metadata();
let dummy_signer = dev::alice();
let call = node_runtime::tx()
.balances()
.transfer_allow_death(dev::bob().public_key().into(), 1000);
let tx_bytes = api
.tx()
.create_v5_partial(&call, &dev::alice().public_key().into(), Default::default())
.await
.unwrap()
.sign(&dummy_signer) // No signature payload is added, but may be inserted into tx extensions.
.into_encoded();
let tx_bytes_cursor = &mut &*tx_bytes;
let decoded =
frame_decode::extrinsics::decode_extrinsic(tx_bytes_cursor, &md, md.types()).unwrap();
assert_eq!(tx_bytes_cursor.len(), 0);
assert_eq!(decoded.version(), 5);
assert_eq!(decoded.ty(), ExtrinsicType::General);
assert_eq!(decoded.pallet_name(), "Balances");
assert_eq!(decoded.call_name(), "transfer_allow_death");
assert!(decoded.transaction_extension_payload().is_some());
// v5 general extrinsics have no signature payload; signature in tx extensions:
assert!(decoded.signature_payload().is_none());
Ok(())
}
@@ -0,0 +1,35 @@
// 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.
#[cfg(all(feature = "unstable-light-client", feature = "chainhead-backend"))]
compile_error!(
"The features 'unstable-light-client' and 'chainhead-backend' cannot be used together"
);
#[cfg(test)]
pub mod utils;
#[cfg(test)]
#[cfg_attr(test, allow(unused_imports))]
use utils::*;
#[cfg(any(
all(test, not(feature = "unstable-light-client")),
all(test, feature = "unstable-light-client-long-running")
))]
mod full_client;
#[cfg(all(test, feature = "unstable-light-client"))]
mod light_client;
#[cfg(test)]
use test_runtime::node_runtime;
// We don't use this dependency, but it's here so that we
// can enable logging easily if need be. Add this to a test
// to enable tracing for it:
//
// tracing_subscriber::fmt::init();
#[cfg(test)]
use tracing_subscriber as _;
@@ -0,0 +1,223 @@
// 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 Initialization and Testing
//!
//! The initialization process of the light client can be slow, especially when
//! it needs to synchronize with a local running node for each individual
//! #[tokio::test] in subxt. To optimize this process, a subset of tests is
//! exposed to ensure the light client remains functional over time. Currently,
//! these tests are placed under an unstable feature flag.
//!
//! Ideally, we would place the light client initialization in a shared static
//! using `OnceCell`. However, during the initialization, tokio::spawn is used
//! to multiplex between subxt requests and node responses. The #[tokio::test]
//! macro internally creates a new Runtime for each individual test. This means
//! that only the first test, which spawns the substrate binary and synchronizes
//! the light client, would have access to the background task. The cleanup process
//! would destroy the spawned background task, preventing subsequent tests from
//! accessing it.
//!
//! To address this issue, we can consider creating a slim proc-macro that
//! transforms the #[tokio::test] into a plain #[test] and runs all the tests
//! on a shared tokio runtime. This approach would allow multiple tests to share
//! the same background task, ensuring consistent access to the light client.
//!
//! For more context see: https://github.com/tokio-rs/tokio/issues/2374.
//!
use crate::utils::node_runtime;
use codec::Compact;
use std::sync::Arc;
use subxt::backend::chain_head::ChainHeadBackend;
use subxt::backend::rpc::RpcClient;
use subxt::dynamic::Value;
use subxt::{client::OnlineClient, config::PolkadotConfig, lightclient::LightClient};
use pezkuwi_subxt_metadata::Metadata;
type Client = OnlineClient<PolkadotConfig>;
/// The Polkadot chainspec.
const POLKADOT_SPEC: &str = include_str!("../../../../artifacts/demo_chain_specs/polkadot.json");
// Check that we can subscribe to non-finalized blocks.
async fn non_finalized_headers_subscription(api: &Client) -> Result<(), subxt::Error> {
let now = std::time::Instant::now();
tracing::trace!("Check non_finalized_headers_subscription");
let mut sub = api.blocks().subscribe_best().await?;
let _block = sub.next().await.unwrap()?;
tracing::trace!("First block took {:?}", now.elapsed());
let _block = sub.next().await.unwrap()?;
tracing::trace!("Second block took {:?}", now.elapsed());
let _block = sub.next().await.unwrap()?;
tracing::trace!("Third block took {:?}", now.elapsed());
Ok(())
}
// Check that we can subscribe to finalized blocks.
async fn finalized_headers_subscription(api: &Client) -> Result<(), subxt::Error> {
let now = std::time::Instant::now();
tracing::trace!("Check finalized_headers_subscription");
let mut sub = api.blocks().subscribe_finalized().await?;
let header = sub.next().await.unwrap()?;
tracing::trace!("First block took {:?}", now.elapsed());
let finalized_hash = api
.backend()
.latest_finalized_block_ref()
.await
.unwrap()
.hash();
tracing::trace!(
"Finalized hash: {:?} took {:?}",
finalized_hash,
now.elapsed()
);
assert_eq!(header.hash(), finalized_hash);
tracing::trace!("Check progress {:?}", now.elapsed());
let _block = sub.next().await.unwrap()?;
tracing::trace!("Second block took {:?}", now.elapsed());
let _block = sub.next().await.unwrap()?;
tracing::trace!("Third block took {:?}", now.elapsed());
let _block = sub.next().await.unwrap()?;
tracing::trace!("Fourth block took {:?}\n", now.elapsed());
Ok(())
}
// Check that we can subscribe to non-finalized blocks.
async fn runtime_api_call(api: &Client) -> Result<(), subxt::Error> {
let now = std::time::Instant::now();
tracing::trace!("Check runtime_api_call");
let mut sub = api.blocks().subscribe_best().await?;
let block = sub.next().await.unwrap()?;
tracing::trace!("First block took {:?}", now.elapsed());
let rt = block.runtime_api().await;
// get metadata via state_call. if it decodes ok, it's probably all good.
let result_bytes = rt.call_raw("Metadata_metadata", None).await?;
let (_, _meta): (Compact<u32>, Metadata) = codec::Decode::decode(&mut &*result_bytes)?;
tracing::trace!("Made runtime API call in {:?}\n", now.elapsed());
Ok(())
}
// Lookup for the `Timestamp::now` plain storage entry.
async fn storage_plain_lookup(api: &Client) -> Result<(), subxt::Error> {
let now = std::time::Instant::now();
tracing::trace!("Check storage_plain_lookup");
let storage_at = api.storage().at_latest().await?;
let addr = node_runtime::storage().timestamp().now();
let entry = storage_at.fetch(addr, ()).await?.decode()?;
tracing::trace!("Storage lookup took {:?}\n", now.elapsed());
assert!(entry > 0);
Ok(())
}
// Make a dynamic constant query for `System::BlockLength`.
async fn dynamic_constant_query(api: &Client) -> Result<(), subxt::Error> {
let now = std::time::Instant::now();
tracing::trace!("Check dynamic_constant_query");
let constant_query = subxt::dynamic::constant::<Value>("System", "BlockLength");
let _value = api.constants().at(&constant_query)?;
tracing::trace!("Dynamic constant query took {:?}\n", now.elapsed());
Ok(())
}
// Fetch a few all events from the latest block and decode them dynamically.
async fn dynamic_events(api: &Client) -> Result<(), subxt::Error> {
let now = std::time::Instant::now();
tracing::trace!("Check dynamic_events");
let events = api.events().at_latest().await?;
for event in events.iter() {
let _event = event?;
tracing::trace!("Event decoding took {:?}", now.elapsed());
}
tracing::trace!("Dynamic events took {:?}\n", now.elapsed());
Ok(())
}
async fn run_test(backend: BackendType) -> Result<(), subxt::Error> {
// Note: This code fetches the chainspec from the Polkadot public RPC node.
// This is not recommended for production use, as it may be slow and unreliable.
// However, this can come in handy for testing purposes.
//
// let chainspec = subxt::utils::fetch_chainspec_from_rpc_node("wss://rpc.polkadot.io:443")
// .await
// .unwrap();
// let chain_config = chainspec.get();
tracing::trace!("Init light client");
let now = std::time::Instant::now();
let (_lc, rpc) = LightClient::relay_chain(POLKADOT_SPEC)?;
let api = match backend {
BackendType::Unstable => {
let backend =
ChainHeadBackend::builder().build_with_background_driver(RpcClient::new(rpc));
let api: OnlineClient<PolkadotConfig> =
OnlineClient::from_backend(Arc::new(backend)).await?;
api
}
BackendType::Legacy => Client::from_rpc_client(rpc).await?,
};
tracing::trace!("Light client initialization took {:?}", now.elapsed());
non_finalized_headers_subscription(&api).await?;
finalized_headers_subscription(&api).await?;
runtime_api_call(&api).await?;
storage_plain_lookup(&api).await?;
dynamic_constant_query(&api).await?;
dynamic_events(&api).await?;
tracing::trace!("Light complete testing took {:?}", now.elapsed());
Ok(())
}
/// Backend type for light client testing.
enum BackendType {
/// Use the unstable backend (ie chainHead).
Unstable,
/// Use the legacy backend.
Legacy,
}
#[tokio::test]
async fn light_client_testing() -> Result<(), subxt::Error> {
tracing_subscriber::fmt::init();
// Run light client test with both backends.
run_test(BackendType::Unstable).await?;
run_test(BackendType::Legacy).await?;
Ok(())
}
@@ -0,0 +1,39 @@
// 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.
pub(crate) use crate::{node_runtime, utils::TestNodeProcess};
use subxt::SubstrateConfig;
use subxt::client::OnlineClient;
use super::node_proc::RpcClientKind;
/// `substrate-node` should be installed on the $PATH. We fall back
/// to also checking for an older `substrate` binary.
const SUBSTRATE_NODE_PATHS: &str = "substrate-node,substrate";
pub async fn test_context_with(authority: String, rpc_client_kind: RpcClientKind) -> TestContext {
let paths =
std::env::var("SUBSTRATE_NODE_PATH").unwrap_or_else(|_| SUBSTRATE_NODE_PATHS.to_string());
let paths: Vec<_> = paths.split(',').map(|p| p.trim()).collect();
let mut proc = TestContext::build(&paths);
proc.with_authority(authority);
proc.with_rpc_client_kind(rpc_client_kind);
proc.spawn::<SubstrateConfig>().await.unwrap()
}
pub type TestConfig = SubstrateConfig;
pub type TestContext = TestNodeProcess<SubstrateConfig>;
pub type TestClient = OnlineClient<SubstrateConfig>;
pub async fn test_context() -> TestContext {
test_context_with("alice".to_string(), RpcClientKind::Legacy).await
}
pub async fn test_context_reconnecting_rpc_client() -> TestContext {
test_context_with("alice".to_string(), RpcClientKind::Reconnecting).await
}
@@ -0,0 +1,21 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
mod context;
mod node_proc;
mod wait_for_blocks;
pub use context::*;
pub use node_proc::TestNodeProcess;
pub use subxt_test_macro::subxt_test;
pub use wait_for_blocks::*;
/// The test timeout is set to 1 second.
/// However, the test is sleeping for 5 seconds.
/// This must cause the test to panic.
#[subxt_test(timeout = 1)]
#[should_panic]
async fn test_pezkuwi_subxt_macro() {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
@@ -0,0 +1,314 @@
// 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 std::cell::RefCell;
use std::ffi::{OsStr, OsString};
use std::sync::Arc;
use std::time::Duration;
use substrate_runner::SubstrateNode;
use subxt::backend::rpc::reconnecting_rpc_client::{ExponentialBackoff, RpcClientBuilder};
use subxt::{
Config, OnlineClient,
backend::{chain_head, legacy, rpc},
};
// The URL that we'll connect to for our tests comes from SUBXT_TEXT_HOST env var,
// defaulting to localhost if not provided. If the env var is set, we won't spawn
// a binary. Note though that some tests expect and modify a fresh state, and so will
// fail. For a similar reason you should also use `--test-threads 1` when running tests
// to reduce the number of conflicts between state altering tests.
const URL_ENV_VAR: &str = "SUBXT_TEST_URL";
fn is_url_provided() -> bool {
std::env::var(URL_ENV_VAR).is_ok()
}
fn get_url(port: Option<u16>) -> String {
match (std::env::var(URL_ENV_VAR).ok(), port) {
(Some(host), None) => host,
(None, Some(port)) => format!("ws://127.0.0.1:{port}"),
(Some(_), Some(_)) => {
panic!("{URL_ENV_VAR} and port provided: only one or the other should exist")
}
(None, None) => {
panic!("No {URL_ENV_VAR} or port was provided, so we don't know where to connect to")
}
}
}
/// Spawn a local substrate node for testing subxt.
pub struct TestNodeProcess<R: Config> {
// Keep a handle to the node; once it's dropped the node is killed.
proc: Option<SubstrateNode>,
// Lazily construct these when asked for.
chainhead_backend: RefCell<Option<OnlineClient<R>>>,
legacy_backend: RefCell<Option<OnlineClient<R>>>,
rpc_client: rpc::RpcClient,
client: OnlineClient<R>,
}
impl<R> TestNodeProcess<R>
where
R: Config,
{
/// Construct a builder for spawning a test node process.
pub fn build<P>(paths: &[P]) -> TestNodeProcessBuilder
where
P: AsRef<OsStr> + Clone,
{
TestNodeProcessBuilder::new(paths)
}
pub async fn restart(mut self) -> Self {
tokio::task::spawn_blocking(move || {
if let Some(proc) = &mut self.proc {
proc.restart().unwrap();
}
self
})
.await
.expect("to succeed")
}
/// Hand back an RPC client connected to the test node which exposes the legacy RPC methods.
pub async fn legacy_rpc_methods(&self) -> legacy::LegacyRpcMethods<R> {
let rpc_client = self.rpc_client.clone();
legacy::LegacyRpcMethods::new(rpc_client)
}
/// Hand back an RPC client connected to the test node which exposes the unstable RPC methods.
pub async fn chainhead_rpc_methods(&self) -> chain_head::ChainHeadRpcMethods<R> {
let rpc_client = self.rpc_client.clone();
chain_head::ChainHeadRpcMethods::new(rpc_client)
}
/// Always return a client using the chainhead backend.
/// Only use for comparing backends; use [`TestNodeProcess::client()`] normally,
/// which enables us to run each test against both backends.
pub async fn chainhead_backend(&self) -> OnlineClient<R> {
if self.chainhead_backend.borrow().is_none() {
let c = build_chainhead_backend(self.rpc_client.clone())
.await
.unwrap();
self.chainhead_backend.replace(Some(c));
}
self.chainhead_backend.borrow().as_ref().unwrap().clone()
}
/// Always return a client using the legacy backend.
/// Only use for comparing backends; use [`TestNodeProcess::client()`] normally,
/// which enables us to run each test against both backends.
pub async fn legacy_backend(&self) -> OnlineClient<R> {
if self.legacy_backend.borrow().is_none() {
let c = build_legacy_backend(self.rpc_client.clone()).await.unwrap();
self.legacy_backend.replace(Some(c));
}
self.legacy_backend.borrow().as_ref().unwrap().clone()
}
/// Returns the subxt client connected to the running node. This client
/// will use the legacy backend by default or the chainhead backend if the
/// "chainhead-backend" feature is enabled, so that we can run each
/// test against both.
pub fn client(&self) -> OnlineClient<R> {
self.client.clone()
}
/// Returns the rpc client connected to the node
pub fn rpc_client(&self) -> rpc::RpcClient {
self.rpc_client.clone()
}
}
/// Kind of rpc client to use in tests
pub enum RpcClientKind {
Legacy,
Reconnecting,
}
/// Construct a test node process.
pub struct TestNodeProcessBuilder {
node_paths: Vec<OsString>,
authority: Option<String>,
rpc_client: RpcClientKind,
}
impl TestNodeProcessBuilder {
pub fn new<P>(node_paths: &[P]) -> TestNodeProcessBuilder
where
P: AsRef<OsStr>,
{
// Check that paths are valid and build up vec.
let mut paths = Vec::new();
for path in node_paths {
let path = path.as_ref();
paths.push(path.to_os_string())
}
Self {
node_paths: paths,
authority: None,
rpc_client: RpcClientKind::Legacy,
}
}
/// Set the testRunner to use a preferred RpcClient impl, ie Legacy or Reconnecting.
pub fn with_rpc_client_kind(&mut self, rpc_client_kind: RpcClientKind) -> &mut Self {
self.rpc_client = rpc_client_kind;
self
}
/// Set the authority dev account for a node in validator mode e.g. --alice.
pub fn with_authority(&mut self, account: String) -> &mut Self {
self.authority = Some(account);
self
}
/// Spawn the substrate node at the given path, and wait for rpc to be initialized.
pub async fn spawn<R>(self) -> Result<TestNodeProcess<R>, String>
where
R: Config,
{
// Only spawn a process if a URL to target wasn't provided as an env var.
let proc = if !is_url_provided() {
let mut node_builder = SubstrateNode::builder();
node_builder.binary_paths(&self.node_paths);
if let Some(authority) = &self.authority {
node_builder.arg(authority.to_lowercase());
}
Some(node_builder.spawn().map_err(|e| e.to_string())?)
} else {
None
};
let ws_url = get_url(proc.as_ref().map(|p| p.ws_port()));
let rpc_client = match self.rpc_client {
RpcClientKind::Legacy => build_rpc_client(&ws_url).await,
RpcClientKind::Reconnecting => build_reconnecting_rpc_client(&ws_url).await,
}
.map_err(|e| format!("Failed to connect to node at {ws_url}: {e}"))?;
// Cache whatever client we build, and None for the other.
#[allow(unused_assignments, unused_mut)]
let mut chainhead_backend = None;
#[allow(unused_assignments, unused_mut)]
let mut legacy_backend = None;
#[cfg(lightclient)]
let client = build_light_client(&proc).await?;
#[cfg(chainhead_backend)]
let client = {
let client = build_chainhead_backend(rpc_client.clone()).await?;
chainhead_backend = Some(client.clone());
client
};
#[cfg(all(not(lightclient), legacy_backend))]
let client = {
let client = build_legacy_backend(rpc_client.clone()).await?;
legacy_backend = Some(client.clone());
client
};
Ok(TestNodeProcess {
proc,
client,
legacy_backend: RefCell::new(legacy_backend),
chainhead_backend: RefCell::new(chainhead_backend),
rpc_client,
})
}
}
async fn build_rpc_client(ws_url: &str) -> Result<rpc::RpcClient, String> {
let rpc_client = rpc::RpcClient::from_insecure_url(ws_url)
.await
.map_err(|e| format!("Cannot construct RPC client: {e}"))?;
Ok(rpc_client)
}
async fn build_reconnecting_rpc_client(ws_url: &str) -> Result<rpc::RpcClient, String> {
let client = RpcClientBuilder::new()
.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
.build(ws_url.to_string())
.await
.map_err(|e| format!("Cannot construct RPC client: {e}"))?;
Ok(rpc::RpcClient::new(client))
}
async fn build_legacy_backend<T: Config>(
rpc_client: rpc::RpcClient,
) -> Result<OnlineClient<T>, String> {
let backend = legacy::LegacyBackend::builder().build(rpc_client);
let client = OnlineClient::from_backend(Arc::new(backend))
.await
.map_err(|e| format!("Cannot construct OnlineClient from backend: {e}"))?;
Ok(client)
}
async fn build_chainhead_backend<T: Config>(
rpc_client: rpc::RpcClient,
) -> Result<OnlineClient<T>, String> {
let backend = chain_head::ChainHeadBackend::builder().build_with_background_driver(rpc_client);
let client = OnlineClient::from_backend(Arc::new(backend))
.await
.map_err(|e| format!("Cannot construct OnlineClient from backend: {e}"))?;
Ok(client)
}
#[cfg(lightclient)]
async fn build_light_client<T: Config>(
maybe_proc: &Option<SubstrateNode>,
) -> Result<OnlineClient<T>, String> {
use subxt::lightclient::{ChainConfig, LightClient};
let proc = if let Some(proc) = maybe_proc {
proc
} else {
return Err("Cannot build light client: no substrate node is running (you can't start a light client when pointing to an external node)".into());
};
// RPC endpoint. Only localhost works.
let ws_url = format!("ws://127.0.0.1:{}", proc.ws_port());
// Wait for a few blocks to be produced using the subxt client.
let client = OnlineClient::<T>::from_url(ws_url.clone())
.await
.map_err(|err| format!("Failed to connect to node rpc at {ws_url}: {err}"))?;
// Wait for at least a few blocks before starting the light client.
// Otherwise, the lightclient might error with
// `"Error when retrieving the call proof: No node available for call proof query"`.
super::wait_for_number_of_blocks(&client, 5).await;
// Now, configure a light client; fetch the chain spec and modify the bootnodes.
let bootnode = format!(
"/ip4/127.0.0.1/tcp/{}/p2p/{}",
proc.p2p_port(),
proc.p2p_address()
);
let chain_spec = subxt::utils::fetch_chainspec_from_rpc_node(ws_url.as_str())
.await
.map_err(|e| format!("Failed to obtain chain spec from local machine: {e}"))?;
let chain_config = ChainConfig::chain_spec(chain_spec.get())
.set_bootnodes([bootnode.as_str()])
.map_err(|e| format!("Light client: cannot update boot nodes: {e}"))?;
// Instantiate the light client.
let (_lightclient, rpc) = LightClient::relay_chain(chain_config)
.map_err(|e| format!("Light client: cannot add relay chain: {e}"))?;
// Instantiate subxt client from this.
build_chainhead_backend(rpc.into()).await
}
@@ -0,0 +1,52 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use subxt::{
Config, OnlineClient, SubstrateConfig, backend::StreamOf, blocks::Block, client::OnlineClientT,
error::BackendError,
};
/// Wait for blocks to be produced before running tests. Specifically, we
/// wait for one more finalized block to be produced, which is important because
/// the first finalized block doesn't have much state etc associated with it.
pub async fn wait_for_blocks<C: Config>(api: &impl OnlineClientT<C>) {
// The current finalized block and the next block.
wait_for_number_of_blocks(api, 2).await;
}
/// Wait for a number of blocks to be produced.
pub async fn wait_for_number_of_blocks<C: Config>(
api: &impl OnlineClientT<C>,
number_of_blocks: usize,
) {
let mut sub = api.blocks().subscribe_finalized().await.unwrap();
for _ in 0..number_of_blocks {
sub.next().await;
}
}
/// Consumes the initial blocks from the stream of blocks to ensure that the stream is up-to-date.
///
/// This may be useful on the unstable backend when the initial blocks may be large
/// and one relies on something to included in finalized block in ner future.
pub async fn consume_initial_blocks(
blocks: &mut StreamOf<
Result<Block<SubstrateConfig, OnlineClient<SubstrateConfig>>, BackendError>,
>,
) {
use tokio::time::{Duration, Instant, interval_at};
const MAX_DURATION: Duration = Duration::from_millis(200);
let mut now = interval_at(Instant::now() + MAX_DURATION, MAX_DURATION);
loop {
tokio::select! {
_ = now.tick() => {
break;
}
_ = blocks.next() => {}
}
}
}
@@ -0,0 +1,20 @@
[package]
name = "subxt-test-macro"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = false
license.workspace = true
repository.workspace = true
documentation.workspace = true
homepage.workspace = true
description = "Subxt integration tests proc-macros"
[lib]
proc-macro = true
[dependencies]
syn = { workspace = true }
quote = { workspace = true }
@@ -0,0 +1,95 @@
// 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.
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
Error,
parse::{Parse, ParseStream},
};
/// Environment variable for setting the timeout for the test.
const SUBXT_TEST_TIMEOUT: &str = "SUBXT_TEST_TIMEOUT";
/// Default timeout for the test.
const DEFAULT_TIMEOUT_SECS: u64 = 60 * 6;
#[proc_macro_attribute]
pub fn subxt_test(attr: TokenStream, item: TokenStream) -> TokenStream {
let subxt_attr = match syn::parse::<SubxtTestAttr>(attr) {
Ok(subxt_attr) => subxt_attr,
Err(err) => return err.into_compile_error().into(),
};
// Timeout is determined by:
// - The timeout attribute if it is set.
// - The SUBXT_TEST_TIMEOUT environment variable if it is set.
// - A default of 6 minutes.
let timeout_duration = subxt_attr.timeout.unwrap_or_else(|| {
std::env::var(SUBXT_TEST_TIMEOUT)
.map(|str| str.parse().unwrap_or(DEFAULT_TIMEOUT_SECS))
.unwrap_or(DEFAULT_TIMEOUT_SECS)
});
let func: syn::ItemFn = match syn::parse(item) {
Ok(func) => func,
Err(err) => return err.into_compile_error().into(),
};
let func_attrs = &func.attrs;
let func_vis = &func.vis;
let func_sig = &func.sig;
let func_block = &func.block;
let mut inner_func_sig = func.sig.clone();
inner_func_sig.ident = format_ident!("{}_inner", inner_func_sig.ident);
let inner_func_name = &inner_func_sig.ident;
let result = quote! {
#[tokio::test]
#( #func_attrs )*
#func_vis #func_sig {
#func_vis #inner_func_sig
#func_block
tokio::time::timeout(std::time::Duration::from_secs(#timeout_duration), #inner_func_name())
.await
.expect("Test timedout")
}
};
result.into()
}
mod keywords {
syn::custom_keyword!(timeout);
}
struct SubxtTestAttr {
timeout: Option<u64>,
}
impl Parse for SubxtTestAttr {
fn parse(input: ParseStream) -> Result<Self, Error> {
if input.is_empty() {
return Ok(Self { timeout: None });
}
let _keyword = input.parse::<keywords::timeout>()?;
input.parse::<syn::token::Eq>()?;
let timeout = input.parse::<syn::LitInt>()?.base10_parse::<u64>()?;
if !input.is_empty() {
return Err(Error::new(
input.span(),
"Expected tokens: `timeout = value`",
));
}
Ok(Self {
timeout: Some(timeout),
})
}
}