fix: Convert vendor/pezkuwi-subxt from submodule to regular directory
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "generate-custom-metadata"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
documentation.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
scale-info = { workspace = true, features = ["std", "bit-vec"] }
|
||||
frame-metadata = { workspace = true, features = ["decode", "current"] }
|
||||
codec = { package = "parity-scale-codec", workspace = true, features = ["std", "derive", "bit-vec"] }
|
||||
@@ -0,0 +1,5 @@
|
||||
# generate-custom-metadata
|
||||
|
||||
A small crate with a binary that creates scale encoded metadata with custom values and writes it to stdout (as raw bytes).
|
||||
|
||||
It also provides dispatch error types that are used in `../ui_tests`.
|
||||
@@ -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.
|
||||
|
||||
use scale_info::{
|
||||
Path, Type, TypeInfo,
|
||||
build::{Fields, Variants},
|
||||
};
|
||||
|
||||
/// See the `ModuleErrorType` in `pezkuwi_subxt_codegen` for more info on the different DispatchError
|
||||
/// types that we've encountered. We need the path to match `sp_runtime::DispatchError`, otherwise
|
||||
/// we could just implement roughly the correct types and derive TypeInfo on them.
|
||||
///
|
||||
/// This type has TypeInfo compatible with the `NamedField` version of the DispatchError.
|
||||
/// This is the oldest version that subxt supports:
|
||||
/// `DispatchError::Module { index: u8, error: u8 }`
|
||||
pub enum NamedFieldDispatchError {}
|
||||
impl TypeInfo for NamedFieldDispatchError {
|
||||
type Identity = Self;
|
||||
fn type_info() -> Type {
|
||||
Type::builder()
|
||||
.path(Path::new("DispatchError", "sp_runtime"))
|
||||
.variant(Variants::new().variant("Module", |builder| {
|
||||
builder
|
||||
.fields(
|
||||
Fields::named()
|
||||
.field(|b| b.name("error").ty::<u8>())
|
||||
.field(|b| b.name("index").ty::<u8>()),
|
||||
)
|
||||
.index(0)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// This type has TypeInfo compatible with the `LegacyError` version of the DispatchError.
|
||||
/// This is the version wasn't around for long:
|
||||
/// `DispatchError::Module ( sp_runtime::ModuleError { index: u8, error: u8 } )`
|
||||
pub enum LegacyDispatchError {}
|
||||
impl TypeInfo for LegacyDispatchError {
|
||||
type Identity = Self;
|
||||
fn type_info() -> Type {
|
||||
struct ModuleError;
|
||||
impl TypeInfo for ModuleError {
|
||||
type Identity = Self;
|
||||
fn type_info() -> Type {
|
||||
Type::builder()
|
||||
.path(Path::new("ModuleError", "sp_runtime"))
|
||||
.composite(
|
||||
Fields::named()
|
||||
.field(|b| b.name("index").ty::<u8>())
|
||||
.field(|b| b.name("error").ty::<u8>()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Type::builder()
|
||||
.path(Path::new("DispatchError", "sp_runtime"))
|
||||
.variant(Variants::new().variant("Module", |builder| {
|
||||
builder
|
||||
.fields(Fields::unnamed().field(|b| b.ty::<ModuleError>()))
|
||||
.index(0)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// This type has TypeInfo compatible with the `ArrayError` version of the DispatchError.
|
||||
/// This is the current version:
|
||||
/// `DispatchError::Module ( sp_runtime::ModuleError { index: u8, error: [u8; 4] } )`
|
||||
pub enum ArrayDispatchError {}
|
||||
impl TypeInfo for ArrayDispatchError {
|
||||
type Identity = Self;
|
||||
fn type_info() -> Type {
|
||||
struct ModuleError;
|
||||
impl TypeInfo for ModuleError {
|
||||
type Identity = Self;
|
||||
fn type_info() -> Type {
|
||||
Type::builder()
|
||||
.path(Path::new("ModuleError", "sp_runtime"))
|
||||
.composite(
|
||||
Fields::named()
|
||||
.field(|b| b.name("index").ty::<u8>())
|
||||
.field(|b| b.name("error").ty::<[u8; 4]>()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Type::builder()
|
||||
.path(Path::new("DispatchError", "sp_runtime"))
|
||||
.variant(Variants::new().variant("Module", |builder| {
|
||||
builder
|
||||
.fields(Fields::unnamed().field(|b| b.ty::<ModuleError>()))
|
||||
.index(0)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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::Encode;
|
||||
use frame_metadata::RuntimeMetadataPrefixed;
|
||||
use frame_metadata::v15::{CustomMetadata, ExtrinsicMetadata, OuterEnums, RuntimeMetadataV15};
|
||||
|
||||
use scale_info::TypeInfo;
|
||||
use scale_info::form::PortableForm;
|
||||
use scale_info::{IntoPortable, meta_type};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub mod dispatch_error;
|
||||
|
||||
/// Generate metadata which contains a `Foo { a: u8, b: &str }` custom value.
|
||||
pub fn metadata_custom_values_foo() -> RuntimeMetadataPrefixed {
|
||||
let mut registry = scale_info::Registry::new();
|
||||
|
||||
// create foo value and type:
|
||||
|
||||
#[derive(TypeInfo, Encode)]
|
||||
struct Foo {
|
||||
a: u8,
|
||||
b: &'static str,
|
||||
}
|
||||
|
||||
let foo_value_metadata: frame_metadata::v15::CustomValueMetadata<PortableForm> = {
|
||||
let value = Foo {
|
||||
a: 42,
|
||||
b: "Have a great day!",
|
||||
};
|
||||
let foo_ty = scale_info::MetaType::new::<Foo>();
|
||||
let foo_ty_id = registry.register_type(&foo_ty);
|
||||
frame_metadata::v15::CustomValueMetadata {
|
||||
ty: foo_ty_id,
|
||||
value: value.encode(),
|
||||
}
|
||||
};
|
||||
|
||||
let invalid_type_id_metadata: frame_metadata::v15::CustomValueMetadata<PortableForm> = {
|
||||
frame_metadata::v15::CustomValueMetadata {
|
||||
ty: u32::MAX.into(),
|
||||
value: vec![0, 1, 2, 3],
|
||||
}
|
||||
};
|
||||
|
||||
// We don't care about the extrinsic type.
|
||||
let extrinsic = ExtrinsicMetadata {
|
||||
version: 0,
|
||||
signed_extensions: vec![],
|
||||
address_ty: meta_type::<()>(),
|
||||
call_ty: meta_type::<()>(),
|
||||
signature_ty: meta_type::<()>(),
|
||||
extra_ty: meta_type::<()>(),
|
||||
};
|
||||
|
||||
let pallets = vec![];
|
||||
let extrinsic = extrinsic.into_portable(&mut registry);
|
||||
|
||||
let unit_ty = registry.register_type(&meta_type::<()>());
|
||||
|
||||
// Metadata needs to contain this DispatchError, since codegen looks for it.
|
||||
registry.register_type(&meta_type::<dispatch_error::ArrayDispatchError>());
|
||||
|
||||
let metadata = RuntimeMetadataV15 {
|
||||
types: registry.into(),
|
||||
pallets,
|
||||
extrinsic,
|
||||
ty: unit_ty,
|
||||
apis: vec![],
|
||||
outer_enums: OuterEnums {
|
||||
call_enum_ty: unit_ty,
|
||||
event_enum_ty: unit_ty,
|
||||
error_enum_ty: unit_ty,
|
||||
},
|
||||
custom: CustomMetadata {
|
||||
// provide foo twice, to make sure nothing breaks in these cases:
|
||||
map: BTreeMap::from_iter([
|
||||
("Foo".into(), foo_value_metadata.clone()),
|
||||
("foo".into(), foo_value_metadata.clone()),
|
||||
("12".into(), foo_value_metadata.clone()),
|
||||
("&Hello".into(), foo_value_metadata),
|
||||
("InvalidTypeId".into(), invalid_type_id_metadata),
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
RuntimeMetadataPrefixed::from(metadata)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
use codec::Encode;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Creates some scale encoded metadata with custom values and writes it out to stdout (as raw bytes)
|
||||
///
|
||||
/// Can be called from the root of the project with: `cargo run --bin generate-custom-metadata > output.scale`.
|
||||
fn main() -> io::Result<()> {
|
||||
let metadata_prefixed = generate_custom_metadata::metadata_custom_values_foo();
|
||||
let stdout = io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
handle.write_all(&metadata_prefixed.encode())?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
+245
@@ -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());
|
||||
}
|
||||
}
|
||||
+431
@@ -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();
|
||||
}
|
||||
+136
@@ -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();
|
||||
}
|
||||
+455
@@ -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);
|
||||
}
|
||||
}
|
||||
*/
|
||||
+167
@@ -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);
|
||||
}
|
||||
+17
@@ -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;
|
||||
+66511
File diff suppressed because one or more lines are too long
+395
@@ -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());
|
||||
}
|
||||
+249
@@ -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;
|
||||
+326
@@ -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(())
|
||||
}
|
||||
+56
@@ -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(())
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
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())
|
||||
}
|
||||
+351
@@ -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;
|
||||
+68
@@ -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(())
|
||||
}
|
||||
*/
|
||||
+128
@@ -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(())
|
||||
}
|
||||
+133
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
+1116
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "subxt-core-no-std-tests"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
subxt-metadata = { path = "../../metadata", default-features = false }
|
||||
subxt-core = { path = "../../core", default-features = false }
|
||||
subxt-signer = { path = "../../signer", default-features = false, features = ["subxt"] }
|
||||
subxt-macro = { path = "../../macro" }
|
||||
codec = { package = "parity-scale-codec", version = "3.6.9", default-features = false, features = ["derive"] }
|
||||
libc_alloc = { version = "1.0.6" }
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
# this shouldn't be needed, it's in workspace.exclude, but still
|
||||
# I get the complaint unless I add it...
|
||||
[workspace]
|
||||
@@ -0,0 +1,7 @@
|
||||
# No-Std Testing Crate
|
||||
|
||||
To test the no-std compatibility of various subxt-* crates, please run:
|
||||
|
||||
```bash
|
||||
cargo build --target thumbv7em-none-eabi
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
nightly
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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.
|
||||
|
||||
#![allow(internal_features)]
|
||||
#![feature(lang_items, alloc_error_handler)]
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
pub extern "C" fn _start(_argc: isize, _argv: *const *const u8) -> isize {
|
||||
compile_test();
|
||||
0
|
||||
}
|
||||
|
||||
#[lang = "eh_personality"]
|
||||
pub extern "C" fn rust_eh_personality() {}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
use libc_alloc::LibcAlloc;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: LibcAlloc = LibcAlloc;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
/// Including code here makes sure it is not pruned.
|
||||
/// We want all code included to compile fine for the `thumbv7em-none-eabi` target.
|
||||
fn compile_test() {
|
||||
// Subxt Metadata compiles:
|
||||
use codec::Decode;
|
||||
let bytes: alloc::vec::Vec<u8> = alloc::vec![0, 1, 2, 3, 4];
|
||||
pezkuwi_subxt_metadata::Metadata::decode(&mut &bytes[..]).expect_err("invalid byte sequence");
|
||||
|
||||
const METADATA: &[u8] = include_bytes!("../../../artifacts/polkadot_metadata_small.scale");
|
||||
pezkuwi_subxt_metadata::Metadata::decode(&mut &METADATA[..]).expect("should be valid metadata");
|
||||
|
||||
// Subxt signer compiles (though nothing much works on this particular nostd target...):
|
||||
// Supported targets: <https://docs.rs/getrandom/latest/getrandom/#supported-targets>
|
||||
use core::str::FromStr;
|
||||
let _ = pezkuwi_subxt_signer::SecretUri::from_str("//Alice/bar");
|
||||
|
||||
// Note: sr25519 needs randomness, but `thumbv7em-none-eabi` isn't supported by
|
||||
// `getrandom`, so we can't sign in nostd on this target.
|
||||
//
|
||||
// use pezkuwi_subxt_signer::sr25519;
|
||||
// let keypair = sr25519::dev::alice();
|
||||
// let message = b"Hello!";
|
||||
// let _signature = keypair.sign(message);
|
||||
// let _public_key = keypair.public_key();
|
||||
|
||||
// Note: `ecdsa` is also not compiling for the `thumbv7em-none-eabi` target owing to
|
||||
// an issue compiling `secp256k1-sys`.
|
||||
//
|
||||
// use pezkuwi_subxt_signer::ecdsa;
|
||||
// let keypair = ecdsa::dev::alice();
|
||||
// let message = b"Hello!";
|
||||
// let _signature = keypair.sign(message);
|
||||
// let _public_key = keypair.public_key();
|
||||
|
||||
// Subxt Core compiles:
|
||||
let _era = pezkuwi_subxt_core::utils::Era::Immortal;
|
||||
}
|
||||
|
||||
#[pezkuwi_subxt_macro::subxt(
|
||||
runtime_metadata_path = "../../artifacts/polkadot_metadata_full.scale",
|
||||
crate = "::pezkuwi_subxt_core"
|
||||
)]
|
||||
pub mod polkadot {}
|
||||
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "substrate-runner"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,3 @@
|
||||
# substrate-runner
|
||||
|
||||
A small crate whose sole purpose is starting up a substrate node on some free port and handing back a handle to it with the port that it started on.
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Io(std::io::Error),
|
||||
CouldNotExtractPort(String),
|
||||
CouldNotExtractP2pAddress(String),
|
||||
CouldNotExtractP2pPort(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::Io(err) => write!(f, "IO error: {err}"),
|
||||
Error::CouldNotExtractPort(log) => write!(
|
||||
f,
|
||||
"could not extract port from running substrate node's stdout: {log}"
|
||||
),
|
||||
Error::CouldNotExtractP2pAddress(log) => write!(
|
||||
f,
|
||||
"could not extract p2p address from running substrate node's stdout: {log}"
|
||||
),
|
||||
Error::CouldNotExtractP2pPort(log) => write!(
|
||||
f,
|
||||
"could not extract p2p port from running substrate node's stdout: {log}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
@@ -0,0 +1,357 @@
|
||||
// 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 error;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::io::{self, BufRead, BufReader, Read};
|
||||
use std::process::{self, Child, Command};
|
||||
|
||||
pub use error::Error;
|
||||
|
||||
type CowStr = Cow<'static, str>;
|
||||
|
||||
pub struct SubstrateNodeBuilder {
|
||||
binary_paths: Vec<OsString>,
|
||||
custom_flags: HashMap<CowStr, Option<CowStr>>,
|
||||
}
|
||||
|
||||
impl Default for SubstrateNodeBuilder {
|
||||
fn default() -> Self {
|
||||
SubstrateNodeBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SubstrateNodeBuilder {
|
||||
/// Configure a new Substrate node.
|
||||
pub fn new() -> Self {
|
||||
SubstrateNodeBuilder {
|
||||
binary_paths: vec![],
|
||||
custom_flags: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide "substrate-node" and "substrate" as binary paths
|
||||
pub fn substrate(&mut self) -> &mut Self {
|
||||
self.binary_paths = vec!["substrate-node".into(), "substrate".into()];
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide "polkadot" as binary path.
|
||||
pub fn polkadot(&mut self) -> &mut Self {
|
||||
self.binary_paths = vec!["polkadot".into()];
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the path to the `substrate` binary; defaults to "substrate-node"
|
||||
/// or "substrate".
|
||||
pub fn binary_paths<Paths, S>(&mut self, paths: Paths) -> &mut Self
|
||||
where
|
||||
Paths: IntoIterator<Item = S>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
self.binary_paths = paths.into_iter().map(|p| p.into()).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a boolean argument like `--alice`
|
||||
pub fn arg(&mut self, s: impl Into<CowStr>) -> &mut Self {
|
||||
self.custom_flags.insert(s.into(), None);
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide an argument with a value.
|
||||
pub fn arg_val(&mut self, key: impl Into<CowStr>, val: impl Into<CowStr>) -> &mut Self {
|
||||
self.custom_flags.insert(key.into(), Some(val.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Spawn the node, handing back an object which, when dropped, will stop it.
|
||||
pub fn spawn(mut self) -> Result<SubstrateNode, Error> {
|
||||
// Try to spawn the binary at each path, returning the
|
||||
// first "ok" or last error that we encountered.
|
||||
let mut res = Err(io::Error::other("No binary path provided"));
|
||||
|
||||
let path = Command::new("mktemp")
|
||||
.arg("-d")
|
||||
.output()
|
||||
.expect("failed to create base dir");
|
||||
let path = String::from_utf8(path.stdout).expect("bad path");
|
||||
let mut bin_path = OsString::new();
|
||||
for binary_path in &self.binary_paths {
|
||||
self.custom_flags
|
||||
.insert("base-path".into(), Some(path.clone().into()));
|
||||
|
||||
res = SubstrateNodeBuilder::try_spawn(binary_path, &self.custom_flags);
|
||||
if res.is_ok() {
|
||||
bin_path.clone_from(binary_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut proc = match res {
|
||||
Ok(proc) => proc,
|
||||
Err(e) => return Err(Error::Io(e)),
|
||||
};
|
||||
|
||||
// Wait for RPC port to be logged (it's logged to stderr).
|
||||
let stderr = proc.stderr.take().unwrap();
|
||||
let running_node = try_find_substrate_port_from_output(stderr);
|
||||
|
||||
let ws_port = running_node.ws_port()?;
|
||||
let p2p_address = running_node.p2p_address()?;
|
||||
let p2p_port = running_node.p2p_port()?;
|
||||
|
||||
Ok(SubstrateNode {
|
||||
binary_path: bin_path,
|
||||
custom_flags: self.custom_flags,
|
||||
proc,
|
||||
ws_port,
|
||||
p2p_address,
|
||||
p2p_port,
|
||||
base_path: path,
|
||||
})
|
||||
}
|
||||
|
||||
// Attempt to spawn a binary with the path/flags given.
|
||||
fn try_spawn(
|
||||
binary_path: &OsString,
|
||||
custom_flags: &HashMap<CowStr, Option<CowStr>>,
|
||||
) -> Result<Child, std::io::Error> {
|
||||
let mut cmd = Command::new(binary_path);
|
||||
|
||||
cmd.env("RUST_LOG", "info,libp2p_tcp=debug,litep2p::tcp=debug")
|
||||
.stdout(process::Stdio::piped())
|
||||
.stderr(process::Stdio::piped())
|
||||
.arg("--dev")
|
||||
.arg("--port=0")
|
||||
// To test archive_* RPC-v2 methods we need the node in archive mode:
|
||||
.arg("--blocks-pruning=archive-canonical")
|
||||
.arg("--state-pruning=archive-canonical");
|
||||
|
||||
for (key, val) in custom_flags {
|
||||
let arg = match val {
|
||||
Some(val) => format!("--{key}={val}"),
|
||||
None => format!("--{key}"),
|
||||
};
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
cmd.spawn()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SubstrateNode {
|
||||
binary_path: OsString,
|
||||
custom_flags: HashMap<CowStr, Option<CowStr>>,
|
||||
proc: process::Child,
|
||||
ws_port: u16,
|
||||
p2p_address: String,
|
||||
p2p_port: u32,
|
||||
base_path: String,
|
||||
}
|
||||
|
||||
impl SubstrateNode {
|
||||
/// Configure and spawn a new [`SubstrateNode`].
|
||||
pub fn builder() -> SubstrateNodeBuilder {
|
||||
SubstrateNodeBuilder::new()
|
||||
}
|
||||
|
||||
/// Return the ID of the running process.
|
||||
pub fn id(&self) -> u32 {
|
||||
self.proc.id()
|
||||
}
|
||||
|
||||
/// Return the port that WS connections are accepted on.
|
||||
pub fn ws_port(&self) -> u16 {
|
||||
self.ws_port
|
||||
}
|
||||
|
||||
/// Return the libp2p address of the running node.
|
||||
pub fn p2p_address(&self) -> String {
|
||||
self.p2p_address.clone()
|
||||
}
|
||||
|
||||
/// Return the libp2p port of the running node.
|
||||
pub fn p2p_port(&self) -> u32 {
|
||||
self.p2p_port
|
||||
}
|
||||
|
||||
/// Kill the process.
|
||||
pub fn kill(&mut self) -> std::io::Result<()> {
|
||||
self.proc.kill()
|
||||
}
|
||||
|
||||
/// restart the node, handing back an object which, when dropped, will stop it.
|
||||
pub fn restart(&mut self) -> Result<(), std::io::Error> {
|
||||
let res: Result<(), io::Error> = self.kill();
|
||||
|
||||
match res {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
self.cleanup();
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
let proc = self.try_spawn()?;
|
||||
|
||||
self.proc = proc;
|
||||
// Wait for RPC port to be logged (it's logged to stderr).
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Attempt to spawn a binary with the path/flags given.
|
||||
fn try_spawn(&mut self) -> Result<Child, std::io::Error> {
|
||||
let mut cmd = Command::new(&self.binary_path);
|
||||
|
||||
cmd.env("RUST_LOG", "info,libp2p_tcp=debug")
|
||||
.stdout(process::Stdio::piped())
|
||||
.stderr(process::Stdio::piped())
|
||||
.arg("--dev");
|
||||
|
||||
for (key, val) in &self.custom_flags {
|
||||
let arg = match val {
|
||||
Some(val) => format!("--{key}={val}"),
|
||||
None => format!("--{key}"),
|
||||
};
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
cmd.arg(format!("--rpc-port={}", self.ws_port));
|
||||
cmd.arg(format!("--port={}", self.p2p_port));
|
||||
cmd.spawn()
|
||||
}
|
||||
|
||||
fn cleanup(&self) {
|
||||
let _ = Command::new("rm")
|
||||
.args(["-rf", &self.base_path])
|
||||
.output()
|
||||
.expect("success");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SubstrateNode {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.kill();
|
||||
self.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// Consume a stderr reader from a spawned substrate command and
|
||||
// locate the port number that is logged out to it.
|
||||
fn try_find_substrate_port_from_output(r: impl Read + Send + 'static) -> SubstrateNodeInfo {
|
||||
let mut port = None;
|
||||
let mut p2p_address = None;
|
||||
let mut p2p_port = None;
|
||||
|
||||
let mut log = String::new();
|
||||
|
||||
for line in BufReader::new(r).lines().take(100) {
|
||||
let line = line.expect("failed to obtain next line from stdout for port discovery");
|
||||
|
||||
log.push_str(&line);
|
||||
log.push('\n');
|
||||
|
||||
// Parse the port lines
|
||||
let line_port = line
|
||||
// oldest message:
|
||||
.rsplit_once("Listening for new connections on 127.0.0.1:")
|
||||
// slightly newer message:
|
||||
.or_else(|| line.rsplit_once("Running JSON-RPC WS server: addr=127.0.0.1:"))
|
||||
// newest message (jsonrpsee merging http and ws servers):
|
||||
.or_else(|| line.rsplit_once("Running JSON-RPC server: addr=127.0.0.1:"))
|
||||
.map(|(_, port_str)| port_str);
|
||||
|
||||
if let Some(ports) = line_port {
|
||||
// If more than one rpc server is started the log will capture multiple ports
|
||||
// such as `addr=127.0.0.1:9944,[::1]:9944`
|
||||
let port_str: String = ports.chars().take_while(|c| c.is_numeric()).collect();
|
||||
|
||||
// expect to have a number here (the chars after '127.0.0.1:') and parse them into a u16.
|
||||
let port_num = port_str
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("valid port expected for log line, got '{port_str}'"));
|
||||
port = Some(port_num);
|
||||
}
|
||||
|
||||
// Parse the p2p address line
|
||||
let line_address = line
|
||||
.rsplit_once("Local node identity is: ")
|
||||
.map(|(_, address_str)| address_str);
|
||||
|
||||
if let Some(line_address) = line_address {
|
||||
let address = line_address.trim_end_matches(|b: char| b.is_ascii_whitespace());
|
||||
p2p_address = Some(address.into());
|
||||
}
|
||||
|
||||
// Parse the p2p port line (present in debug logs)
|
||||
let p2p_port_line = line
|
||||
// oldest message:
|
||||
.rsplit_once("New listen address: /ip4/127.0.0.1/tcp/")
|
||||
// slightly newer message:
|
||||
.or_else(|| line.rsplit_once("New listen address address=/ip4/127.0.0.1/tcp/"))
|
||||
// Newest message using the litep2p backend:
|
||||
.or_else(|| {
|
||||
// The line looks like:
|
||||
// `start tcp transport listen_addresses=["/ip6/::/tcp/30333", "/ip4/0.0.0.0/tcp/30333"]`
|
||||
// we'll split once to find the line itself and then again to find the ipv4 port.
|
||||
line.rsplit_once("start tcp transport listen_addresses=")
|
||||
.and_then(|(_, after)| after.split_once("/ip4/0.0.0.0/tcp/"))
|
||||
})
|
||||
.map(|(_, address_str)| address_str);
|
||||
|
||||
if let Some(line_port) = p2p_port_line {
|
||||
// trim non-numeric chars from the end of the port part of the line.
|
||||
let port_str = line_port.trim_end_matches(|b: char| !b.is_ascii_digit());
|
||||
|
||||
// expect to have a number here (the chars after '127.0.0.1:') and parse them into a u16.
|
||||
let port_num = port_str
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("valid port expected for log line, got '{port_str}'"));
|
||||
p2p_port = Some(port_num);
|
||||
}
|
||||
|
||||
if port.is_some() && p2p_address.is_some() && p2p_port.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SubstrateNodeInfo {
|
||||
ws_port: port,
|
||||
p2p_address,
|
||||
p2p_port,
|
||||
log,
|
||||
}
|
||||
}
|
||||
|
||||
/// Data extracted from the running node's stdout.
|
||||
#[derive(Debug)]
|
||||
pub struct SubstrateNodeInfo {
|
||||
ws_port: Option<u16>,
|
||||
p2p_address: Option<String>,
|
||||
p2p_port: Option<u32>,
|
||||
log: String,
|
||||
}
|
||||
|
||||
impl SubstrateNodeInfo {
|
||||
pub fn ws_port(&self) -> Result<u16, Error> {
|
||||
self.ws_port
|
||||
.ok_or_else(|| Error::CouldNotExtractPort(self.log.clone()))
|
||||
}
|
||||
|
||||
pub fn p2p_address(&self) -> Result<String, Error> {
|
||||
self.p2p_address
|
||||
.clone()
|
||||
.ok_or_else(|| Error::CouldNotExtractP2pAddress(self.log.clone()))
|
||||
}
|
||||
|
||||
pub fn p2p_port(&self) -> Result<u32, Error> {
|
||||
self.p2p_port
|
||||
.ok_or_else(|| Error::CouldNotExtractP2pPort(self.log.clone()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "test-runtime"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
pezkuwi-subxt = { workspace = true, features = ["native"] }
|
||||
|
||||
[build-dependencies]
|
||||
substrate-runner = { workspace = true }
|
||||
impl-serde = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
tokio-util = { workspace = true, features = ["compat"] }
|
||||
which = { workspace = true }
|
||||
jsonrpsee = { workspace = true, features = [
|
||||
"async-client",
|
||||
"client-ws-transport-tls",
|
||||
] }
|
||||
hex = { workspace = true }
|
||||
codec = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["subxt"]
|
||||
@@ -0,0 +1,10 @@
|
||||
# test-runtime
|
||||
|
||||
The logic for this crate exists mainly in the `build.rs` file.
|
||||
|
||||
At compile time, this crate will:
|
||||
- Spin up a local `substrate` binary (set the `SUBSTRATE_NODE_PATH` env var to point to a custom binary, otherwise it'll look for `substrate` on your PATH).
|
||||
- Obtain metadata from this node.
|
||||
- Export the metadata and a `node_runtime` module which has been annotated using the `subxt` proc macro and is based off the above metadata.
|
||||
|
||||
The reason for doing this is that our integration tests (which also spin up a Substrate node) can then use the generated `subxt` types from the exact node being tested against, so that we don't have to worry about metadata getting out of sync with the binary under test.
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
// 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, Encode};
|
||||
use std::{env, fs, path::Path};
|
||||
use substrate_runner::{Error as SubstrateNodeError, SubstrateNode};
|
||||
|
||||
// This variable accepts a single binary name or comma separated list.
|
||||
static SUBSTRATE_BIN_ENV_VAR: &str = "SUBSTRATE_NODE_PATH";
|
||||
|
||||
const V15_METADATA_VERSION: u32 = 15;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
run().await;
|
||||
}
|
||||
|
||||
async fn run() {
|
||||
// Select substrate binary to run based on env var.
|
||||
let substrate_bins: String =
|
||||
env::var(SUBSTRATE_BIN_ENV_VAR).unwrap_or_else(|_| "substrate-node,substrate".to_owned());
|
||||
let substrate_bins_vec: Vec<&str> = substrate_bins.split(',').map(|s| s.trim()).collect();
|
||||
|
||||
let mut node_builder = SubstrateNode::builder();
|
||||
node_builder.binary_paths(substrate_bins_vec.iter());
|
||||
|
||||
let node = match node_builder.spawn() {
|
||||
Ok(node) => node,
|
||||
Err(SubstrateNodeError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
panic!(
|
||||
"A substrate binary should be installed on your path for testing purposes. \
|
||||
See https://github.com/paritytech/subxt/tree/master#integration-testing"
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Cannot spawn substrate command from any of {substrate_bins_vec:?}: {e}")
|
||||
}
|
||||
};
|
||||
|
||||
let port = node.ws_port();
|
||||
let out_dir_env_var = env::var_os("OUT_DIR");
|
||||
let out_dir = out_dir_env_var.as_ref().unwrap().to_str().unwrap();
|
||||
|
||||
let stable_metadata_path =
|
||||
download_and_save_metadata(V15_METADATA_VERSION, port, out_dir, "v15")
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("Cannot download & save v15 metadata: {e}"));
|
||||
|
||||
// Write out our expression to generate the runtime API to a file. Ideally, we'd just write this code
|
||||
// in lib.rs, but we must pass a string literal (and not `concat!(..)`) as an arg to `runtime_metadata_path`,
|
||||
// and so we need to spit it out here and include it verbatim instead.
|
||||
let runtime_api_contents = format!(
|
||||
r#"
|
||||
/// Generated types for the locally running Substrate node using V15 metadata.
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "{stable_metadata_path}",
|
||||
derive_for_all_types = "Eq, PartialEq",
|
||||
)]
|
||||
pub mod node_runtime {{}}
|
||||
"#
|
||||
);
|
||||
let runtime_path = Path::new(&out_dir).join("runtime.rs");
|
||||
fs::write(runtime_path, runtime_api_contents).expect("Couldn't write runtime rust output");
|
||||
|
||||
for substrate_node_path in substrate_bins_vec {
|
||||
let Ok(full_path) = which::which(substrate_node_path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Re-build if the substrate binary we're pointed to changes (mtime):
|
||||
println!("cargo:rerun-if-changed={}", full_path.to_string_lossy());
|
||||
}
|
||||
|
||||
// Re-build if we point to a different substrate binary:
|
||||
println!("cargo:rerun-if-env-changed={SUBSTRATE_BIN_ENV_VAR}");
|
||||
// Re-build if this file changes:
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
||||
// Download metadata from binary. Avoid Subxt dep on `subxt::rpc::types::Bytes`and just impl here.
|
||||
// This may at least prevent this script from running so often (ie whenever we change Subxt).
|
||||
// If there's an error, we return a string for it.
|
||||
async fn download_and_save_metadata(
|
||||
version: u32,
|
||||
port: u16,
|
||||
out_dir: &str,
|
||||
suffix: &str,
|
||||
) -> Result<String, String> {
|
||||
// Encode version
|
||||
let bytes = version.encode();
|
||||
let version: String = format!("0x{}", hex::encode(&bytes));
|
||||
|
||||
// Connect to the client and request metadata
|
||||
let raw: String = {
|
||||
use client::ClientT;
|
||||
client::build(&format!("ws://localhost:{port}"))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to node: {e}"))?
|
||||
.request(
|
||||
"state_call",
|
||||
client::rpc_params!["Metadata_metadata_at_version", &version],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to obtain metadata from node: {e}"))?
|
||||
};
|
||||
|
||||
// Decode the raw metadata
|
||||
let raw_bytes = hex::decode(raw.trim_start_matches("0x"))
|
||||
.map_err(|e| format!("Failed to hex-decode metadata: {e}"))?;
|
||||
let bytes: Option<Vec<u8>> = Decode::decode(&mut &raw_bytes[..])
|
||||
.map_err(|e| format!("Failed to decode metadata bytes: {e}"))?;
|
||||
let metadata_bytes = bytes.ok_or_else(|| "Metadata version not found".to_string())?;
|
||||
|
||||
// Save metadata to a file
|
||||
let metadata_path =
|
||||
Path::new(&out_dir).join(format!("test_node_runtime_metadata_{suffix}.scale"));
|
||||
fs::write(&metadata_path, metadata_bytes)
|
||||
.map_err(|e| format!("Couldn't write metadata output: {e}"))?;
|
||||
|
||||
// Convert path to string and return
|
||||
metadata_path
|
||||
.to_str()
|
||||
.ok_or_else(|| "Path to metadata should be stringifiable".to_string())
|
||||
.map(|s| s.to_owned())
|
||||
}
|
||||
|
||||
// Use jsonrpsee to obtain metadata from the node.
|
||||
mod client {
|
||||
use jsonrpsee::client_transport::ws::EitherStream;
|
||||
pub use jsonrpsee::{
|
||||
client_transport::ws::{self, Url, WsTransportClientBuilder},
|
||||
core::client::{Client, Error},
|
||||
};
|
||||
use tokio_util::compat::Compat;
|
||||
|
||||
pub use jsonrpsee::core::{client::ClientT, rpc_params};
|
||||
pub type Sender = ws::Sender<Compat<EitherStream>>;
|
||||
pub type Receiver = ws::Receiver<Compat<EitherStream>>;
|
||||
|
||||
/// Build WS RPC client from URL
|
||||
pub async fn build(url: &str) -> Result<Client, Error> {
|
||||
let (sender, receiver) = ws_transport(url).await?;
|
||||
Ok(Client::builder().build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
async fn ws_transport(url: &str) -> Result<(Sender, Receiver), Error> {
|
||||
let url = Url::parse(url).map_err(|e| Error::Transport(e.into()))?;
|
||||
WsTransportClientBuilder::default()
|
||||
.build(url)
|
||||
.await
|
||||
.map_err(|e| Error::Transport(e.into()))
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
/// The SCALE encoded metadata obtained from a local run of a substrate node.
|
||||
pub static METADATA: &[u8] = include_bytes!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/test_node_runtime_metadata_v15.scale"
|
||||
));
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/runtime.rs"));
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "ui-tests"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
scale-info = { workspace = true, features = ["bit-vec"] }
|
||||
frame-metadata = { workspace = true }
|
||||
codec = { package = "parity-scale-codec", workspace = true, features = ["derive", "bit-vec"] }
|
||||
pezkuwi-subxt = { workspace = true, features = ["native", "jsonrpsee", "runtime-wasm-path"] }
|
||||
pezkuwi-subxt-metadata = { workspace = true }
|
||||
subxt-utils-stripmetadata = { workspace = true }
|
||||
generate-custom-metadata = { path = "../generate-custom-metadata" }
|
||||
@@ -0,0 +1,48 @@
|
||||
use codec::{Decode};
|
||||
use subxt::{config::substrate::H256, OfflineClient, PolkadotConfig};
|
||||
use pezkuwi_subxt_metadata::Metadata;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../../../../artifacts/metadata_with_custom_values.scale", derive_for_all_types = "Eq, PartialEq")]
|
||||
pub mod node {}
|
||||
use node::runtime_types::generate_custom_metadata::Foo;
|
||||
|
||||
fn main() {
|
||||
let api = construct_offline_client();
|
||||
|
||||
let expected_foo = Foo {
|
||||
a: 42,
|
||||
b: "Have a great day!".into(),
|
||||
};
|
||||
|
||||
// static query:
|
||||
let foo_address = node::custom().foo();
|
||||
let foo = api.custom_values().at(&foo_address).unwrap();
|
||||
assert_eq!(foo, expected_foo);
|
||||
|
||||
// dynamic query:
|
||||
let foo_address = subxt::dynamic::custom_value::<Foo>("Foo");
|
||||
let foo = api.custom_values().at(&foo_address).unwrap();
|
||||
assert_eq!(foo, expected_foo);
|
||||
|
||||
// static query for some custom value that has an invalid type id: (we can still access the bytes)
|
||||
let custom_bytes = api.custom_values().bytes_at("InvalidTypeId").unwrap();
|
||||
assert_eq!(vec![0,1,2,3], custom_bytes);
|
||||
}
|
||||
|
||||
fn construct_offline_client() -> OfflineClient<PolkadotConfig> {
|
||||
let genesis_hash = {
|
||||
let h = "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3";
|
||||
let bytes = hex::decode(h).unwrap();
|
||||
H256::from_slice(&bytes)
|
||||
};
|
||||
let runtime_version = subxt::client::RuntimeVersion {
|
||||
spec_version: 9370,
|
||||
transaction_version: 20,
|
||||
};
|
||||
|
||||
let metadata = {
|
||||
let bytes = std::fs::read("../../../../artifacts/metadata_with_custom_values.scale").unwrap();
|
||||
Metadata::decode(&mut &*bytes).unwrap()
|
||||
};
|
||||
OfflineClient::<PolkadotConfig>::new(genesis_hash, runtime_version, metadata)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
use codec::{Decode, Encode};
|
||||
use subxt::utils::AccountId32;
|
||||
|
||||
#[derive(Encode, Decode, subxt::ext::scale_encode::EncodeAsType, subxt::ext::scale_decode::DecodeAsType, Debug)]
|
||||
#[encode_as_type(crate_path = "subxt::ext::scale_encode")]
|
||||
#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
|
||||
pub struct CustomAddress(u16);
|
||||
|
||||
#[derive(Encode, Decode, subxt::ext::scale_encode::EncodeAsType, subxt::ext::scale_decode::DecodeAsType, Debug)]
|
||||
#[encode_as_type(crate_path = "subxt::ext::scale_encode")]
|
||||
#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
|
||||
pub struct Generic<T>(T);
|
||||
|
||||
#[derive(Encode, Decode, subxt::ext::scale_encode::EncodeAsType, subxt::ext::scale_decode::DecodeAsType, Debug)]
|
||||
#[encode_as_type(crate_path = "subxt::ext::scale_encode")]
|
||||
#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
|
||||
pub struct Second<T, U>(T, U);
|
||||
|
||||
#[derive(Encode, Decode, Debug)]
|
||||
pub struct DoesntImplEncodeDecodeAsType(u16);
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "sp_runtime::multiaddress::MultiAddress<A, B>",
|
||||
// Discarding both params:
|
||||
with = "crate::CustomAddress"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime {}
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "sp_runtime::multiaddress::MultiAddress<A, B>",
|
||||
// Discarding second param:
|
||||
with = "crate::Generic<A>"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime2 {}
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "sp_runtime::multiaddress::MultiAddress<A, B>",
|
||||
// Discarding first param:
|
||||
with = "crate::Generic<B>"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime3 {}
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "sp_runtime::multiaddress::MultiAddress<A, B>",
|
||||
// Swapping params:
|
||||
with = "crate::Second<B, A>"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime4 {}
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "sp_runtime::multiaddress::MultiAddress",
|
||||
// Ignore input params and just use concrete types on output:
|
||||
with = "crate::Second<bool, ::std::vec::Vec<u8>>"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime5 {}
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "sp_runtime::multiaddress::MultiAddress<A, B>",
|
||||
// We can put a static type in, too:
|
||||
with = "crate::Second<B, u16>"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime6 {}
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "sp_runtime::multiaddress::MultiAddress<A, B>",
|
||||
// Check that things can be wrapped in our Static type:
|
||||
with = "::pezkuwi_subxt::utils::Static<crate::DoesntImplEncodeDecodeAsType>"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime7 {}
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "sp_runtime::multiaddress::MultiAddress<A, B>",
|
||||
// Recursive type param substitution should work too (swapping out nested A and B):
|
||||
with = "::pezkuwi_subxt::utils::Static<crate::Second<A, B>>"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime8 {}
|
||||
|
||||
fn main() {
|
||||
// We assume Polkadot's config of MultiAddress<AccountId32, ()> here
|
||||
let _ = node_runtime::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(CustomAddress(1337), 123);
|
||||
|
||||
let _ = node_runtime2::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(Generic(AccountId32::from([0x01;32])), 123);
|
||||
|
||||
let _ = node_runtime3::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(Generic(()), 123);
|
||||
|
||||
let _ = node_runtime4::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(Second((), AccountId32::from([0x01;32])), 123);
|
||||
|
||||
let _ = node_runtime5::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(Second(true, vec![1u8, 2u8]), 123);
|
||||
|
||||
let _ = node_runtime6::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(Second((), 1234u16), 123);
|
||||
|
||||
let _ = node_runtime7::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(subxt::utils::Static(DoesntImplEncodeDecodeAsType(1337)), 123);
|
||||
|
||||
let _ = node_runtime8::tx()
|
||||
.balances()
|
||||
.transfer_allow_death(subxt::utils::Static(Second(AccountId32::from([0x01;32]), ())), 123);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#[subxt::subxt(runtime_metadata_path = "../../../../artifacts/polkadot_metadata_tiny.scale")]
|
||||
pub mod node_runtime {
|
||||
pub struct SomeStruct;
|
||||
pub enum SomeEnum {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
pub trait SomeTrait {
|
||||
fn some_func(&self) -> u32;
|
||||
}
|
||||
impl SomeTrait for SomeStruct {
|
||||
fn some_func(&self) -> u32 {
|
||||
1
|
||||
}
|
||||
}
|
||||
impl SomeTrait for SomeEnum {
|
||||
fn some_func(&self) -> u32 {
|
||||
2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
use node_runtime::SomeTrait;
|
||||
|
||||
let unit = node_runtime::SomeStruct;
|
||||
assert_eq!(unit.some_func(), 1);
|
||||
let enumeration = node_runtime::SomeEnum::A;
|
||||
assert_eq!(enumeration.some_func(), 2);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#[subxt::subxt(runtime_path = "../../../../artifacts/westend_runtime.wasm")]
|
||||
mod runtime {}
|
||||
|
||||
#[subxt::subxt(runtime_path = "../../../../artifacts/westend_runtime.compact.compressed.wasm")]
|
||||
mod runtime_compressed {}
|
||||
|
||||
fn main() {
|
||||
use runtime;
|
||||
use runtime_compressed;
|
||||
|
||||
let _ = runtime::system::events::CodeUpdated;
|
||||
let _ = runtime_compressed::system::events::CodeUpdated;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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::generate_metadata_from_pallets_custom_dispatch_error;
|
||||
|
||||
use generate_custom_metadata::dispatch_error::{
|
||||
ArrayDispatchError, LegacyDispatchError, NamedFieldDispatchError,
|
||||
};
|
||||
|
||||
use frame_metadata::RuntimeMetadataPrefixed;
|
||||
|
||||
pub fn metadata_array_dispatch_error() -> RuntimeMetadataPrefixed {
|
||||
generate_metadata_from_pallets_custom_dispatch_error::<ArrayDispatchError>(vec![], vec![])
|
||||
}
|
||||
|
||||
pub fn metadata_legacy_dispatch_error() -> RuntimeMetadataPrefixed {
|
||||
generate_metadata_from_pallets_custom_dispatch_error::<LegacyDispatchError>(vec![], vec![])
|
||||
}
|
||||
|
||||
pub fn metadata_named_field_dispatch_error() -> RuntimeMetadataPrefixed {
|
||||
generate_metadata_from_pallets_custom_dispatch_error::<NamedFieldDispatchError>(vec![], vec![])
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#[subxt::subxt()]
|
||||
pub mod node_runtime {}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,7 @@
|
||||
error: At least one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' or 'runtime_path` can be provided
|
||||
--> src/incorrect/need_url_or_path.rs:1:1
|
||||
|
|
||||
1 | #[subxt::subxt()]
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the attribute macro `subxt::subxt` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "sp_runtime::multiaddress::Event",
|
||||
with = "crate::MyEvent"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime {}
|
||||
|
||||
fn main() {}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
error: Type `Event` does not exist at path `sp_runtime::multiaddress::Event`
|
||||
|
||||
A type with the same name is present at:
|
||||
frame_system::pallet::Event
|
||||
pallet_indices::pallet::Event
|
||||
pallet_balances::pallet::Event
|
||||
pallet_parameters::pallet::Event
|
||||
pallet_transaction_payment::pallet::Event
|
||||
pallet_offences::pallet::Event
|
||||
pallet_session::historical::pallet::Event
|
||||
pallet_session::pallet::Event
|
||||
pallet_grandpa::pallet::Event
|
||||
pallet_treasury::pallet::Event
|
||||
pallet_conviction_voting::pallet::Event
|
||||
pallet_ranked_collective::pallet::Event
|
||||
pallet_whitelist::pallet::Event
|
||||
polkadot_runtime_common::claims::pallet::Event
|
||||
pallet_utility::pallet::Event
|
||||
pallet_identity::pallet::Event
|
||||
pallet_society::pallet::Event
|
||||
pallet_recovery::pallet::Event
|
||||
pallet_vesting::pallet::Event
|
||||
pallet_scheduler::pallet::Event
|
||||
pallet_proxy::pallet::Event
|
||||
pallet_multisig::pallet::Event
|
||||
pallet_preimage::pallet::Event
|
||||
pallet_asset_rate::pallet::Event
|
||||
pallet_bounties::pallet::Event
|
||||
pallet_child_bounties::pallet::Event
|
||||
pallet_nis::pallet::Event
|
||||
pallet_balances::pallet::Event
|
||||
polkadot_runtime_parachains::inclusion::pallet::Event
|
||||
polkadot_runtime_parachains::paras::pallet::Event
|
||||
polkadot_runtime_parachains::hrmp::pallet::Event
|
||||
polkadot_runtime_parachains::disputes::pallet::Event
|
||||
pallet_message_queue::pallet::Event
|
||||
polkadot_runtime_parachains::on_demand::pallet::Event
|
||||
polkadot_runtime_common::paras_registrar::pallet::Event
|
||||
polkadot_runtime_common::slots::pallet::Event
|
||||
polkadot_runtime_common::auctions::pallet::Event
|
||||
polkadot_runtime_common::crowdloan::pallet::Event
|
||||
polkadot_runtime_parachains::coretime::pallet::Event
|
||||
pallet_migrations::pallet::Event
|
||||
pallet_xcm::pallet::Event
|
||||
polkadot_runtime_common::identity_migrator::pallet::Event
|
||||
polkadot_runtime_common::assigned_slots::pallet::Event
|
||||
rococo_runtime::validator_manager::pallet::Event
|
||||
pallet_state_trie_migration::pallet::Event
|
||||
pallet_root_testing::pallet::Event
|
||||
pallet_sudo::pallet::Event
|
||||
--> src/incorrect/substitute_at_wrong_path.rs:1:1
|
||||
|
|
||||
1 | / #[subxt::subxt(
|
||||
2 | | runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
3 | | substitute_type(
|
||||
4 | | path = "sp_runtime::multiaddress::Event",
|
||||
... |
|
||||
7 | | )]
|
||||
| |__^
|
||||
|
|
||||
= note: this error originates in the attribute macro `subxt::subxt` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
substitute_type(
|
||||
path = "frame_support::dispatch::DispatchInfo",
|
||||
with = "my_mod::DispatchInfo"
|
||||
)
|
||||
)]
|
||||
pub mod node_runtime {}
|
||||
|
||||
fn main() {}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
error: Type `DispatchInfo` does not exist at path `frame_support::dispatch::DispatchInfo`
|
||||
|
||||
There is no Type with name `DispatchInfo` in the provided metadata.
|
||||
--> src/incorrect/substitute_path_not_absolute.rs:1:1
|
||||
|
|
||||
1 | / #[subxt::subxt(
|
||||
2 | | runtime_metadata_path = "../../../../artifacts/polkadot_metadata_small.scale",
|
||||
3 | | substitute_type(
|
||||
4 | | path = "frame_support::dispatch::DispatchInfo",
|
||||
... |
|
||||
7 | | )]
|
||||
| |__^
|
||||
|
|
||||
= note: this error originates in the attribute macro `subxt::subxt` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -0,0 +1,14 @@
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_tiny.scale",
|
||||
runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443"
|
||||
)]
|
||||
pub mod node_runtime {}
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_tiny.scale",
|
||||
runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443",
|
||||
runtime_path = "../../../../artifacts/westend_runtime.wasm"
|
||||
)]
|
||||
pub mod node_runtime2 {}
|
||||
|
||||
fn main() {}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
error: Only one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' or 'runtime_path` can be provided
|
||||
--> src/incorrect/url_and_path_provided.rs:1:1
|
||||
|
|
||||
1 | / #[subxt::subxt(
|
||||
2 | | runtime_metadata_path = "../../../../artifacts/polkadot_metadata_tiny.scale",
|
||||
3 | | runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443"
|
||||
4 | | )]
|
||||
| |__^
|
||||
|
|
||||
= note: this error originates in the attribute macro `subxt::subxt` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error: Only one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' or `runtime_path` must be provided
|
||||
--> src/incorrect/url_and_path_provided.rs:7:1
|
||||
|
|
||||
7 | / #[subxt::subxt(
|
||||
8 | | runtime_metadata_path = "../../../../artifacts/polkadot_metadata_tiny.scale",
|
||||
9 | | runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443",
|
||||
10 | | runtime_path = "../../../../artifacts/westend_runtime.wasm"
|
||||
11 | | )]
|
||||
| |__^
|
||||
|
|
||||
= note: this error originates in the attribute macro `subxt::subxt` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
// 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(test)]
|
||||
|
||||
//! UI test set uses [`trybuild`](https://docs.rs/trybuild/latest/trybuild/index.html) to
|
||||
//! check whether expected valid examples of code compile correctly, and for incorrect ones
|
||||
//! errors are helpful and valid (e.g. have correct spans).
|
||||
//!
|
||||
//!
|
||||
//! Use with `TRYBUILD=overwrite` after updating codebase (see `trybuild` docs for more details on that)
|
||||
//! to automatically regenerate `stderr` files, but don't forget to check that new files make sense.
|
||||
|
||||
mod dispatch_errors;
|
||||
mod runtime_apis;
|
||||
mod storage;
|
||||
mod utils;
|
||||
|
||||
use crate::utils::MetadataTestRunner;
|
||||
use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed};
|
||||
use pezkuwi_subxt_utils_stripmetadata::StripMetadata;
|
||||
|
||||
// Each of these tests leads to some rust code being compiled and
|
||||
// executed to test that compilation is successful (or errors in the
|
||||
// way that we'd expect).
|
||||
|
||||
fn strip_metadata<Pallets, Apis>(
|
||||
metadata: &mut RuntimeMetadataPrefixed,
|
||||
pallets: Pallets,
|
||||
apis: Apis,
|
||||
) where
|
||||
Pallets: Fn(&str) -> bool,
|
||||
Apis: Fn(&str) -> bool,
|
||||
{
|
||||
match &mut metadata.1 {
|
||||
RuntimeMetadata::V14(m) => m.strip_metadata(pallets, apis),
|
||||
RuntimeMetadata::V15(m) => m.strip_metadata(pallets, apis),
|
||||
RuntimeMetadata::V16(m) => m.strip_metadata(pallets, apis),
|
||||
m => panic!(
|
||||
"Metadata should be V14, V15 or V16, but is V{}",
|
||||
m.version()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_tests() {
|
||||
let mut m = MetadataTestRunner::default();
|
||||
let t = trybuild::TestCases::new();
|
||||
|
||||
t.pass("src/correct/*.rs");
|
||||
// Check that storage maps with no keys are handled properly.
|
||||
t.pass(
|
||||
m.new_test_case()
|
||||
.name("storage_map_no_keys")
|
||||
.build(storage::metadata_storage_map_no_keys()),
|
||||
);
|
||||
|
||||
// Check runtime APIs with _ in method names work
|
||||
t.pass(
|
||||
m.new_test_case()
|
||||
.name("runtime_api_underscore_method_name")
|
||||
.build(runtime_apis::metadata_runtime_api_underscore_method_name()),
|
||||
);
|
||||
|
||||
// Test that the codegen can handle the different types of DispatchError.
|
||||
t.pass(
|
||||
m.new_test_case()
|
||||
.name("named_field_dispatch_error")
|
||||
.build(dispatch_errors::metadata_named_field_dispatch_error()),
|
||||
);
|
||||
t.pass(
|
||||
m.new_test_case()
|
||||
.name("legacy_dispatch_error")
|
||||
.build(dispatch_errors::metadata_legacy_dispatch_error()),
|
||||
);
|
||||
t.pass(
|
||||
m.new_test_case()
|
||||
.name("array_dispatch_error")
|
||||
.build(dispatch_errors::metadata_array_dispatch_error()),
|
||||
);
|
||||
|
||||
// Test retaining only specific pallets and ensure that works.
|
||||
for pallet in ["Babe", "Claims", "Grandpa", "Balances"] {
|
||||
let mut metadata = MetadataTestRunner::load_metadata();
|
||||
strip_metadata(&mut metadata, |p| p == pallet, |_| true);
|
||||
|
||||
t.pass(
|
||||
m.new_test_case()
|
||||
.name(format!("retain_pallet_{pallet}"))
|
||||
.build(metadata),
|
||||
);
|
||||
}
|
||||
|
||||
// Test retaining only specific runtime APIs to ensure that works.
|
||||
for runtime_api in ["Core", "Metadata"] {
|
||||
let mut metadata = MetadataTestRunner::load_metadata();
|
||||
strip_metadata(&mut metadata, |_| true, |r| r == runtime_api);
|
||||
|
||||
t.pass(
|
||||
m.new_test_case()
|
||||
.name(format!("retain_runtime_api_{runtime_api}"))
|
||||
.build(metadata),
|
||||
);
|
||||
}
|
||||
|
||||
// Validation should succeed when metadata we codegen from is stripped and
|
||||
// client state is full:
|
||||
{
|
||||
let mut metadata = MetadataTestRunner::load_metadata();
|
||||
strip_metadata(
|
||||
&mut metadata,
|
||||
|p| ["Babe", "Claims"].contains(&p),
|
||||
|r| ["Core", "Metadata"].contains(&r),
|
||||
);
|
||||
|
||||
t.pass(
|
||||
m.new_test_case()
|
||||
.name("stripped_metadata_validates_against_full")
|
||||
.validation_metadata(MetadataTestRunner::load_metadata())
|
||||
.build(metadata),
|
||||
);
|
||||
}
|
||||
|
||||
// Finally as a sanity check, codegen against stripped metadata should
|
||||
// _not_ compare valid against client with differently stripped metadata.
|
||||
{
|
||||
let mut codegen_metadata = MetadataTestRunner::load_metadata();
|
||||
strip_metadata(
|
||||
&mut codegen_metadata,
|
||||
|p| ["Babe", "Claims"].contains(&p),
|
||||
|r| ["Core", "Metadata"].contains(&r),
|
||||
);
|
||||
let mut validation_metadata = MetadataTestRunner::load_metadata();
|
||||
strip_metadata(
|
||||
&mut validation_metadata,
|
||||
|p| p != "Claims",
|
||||
|r| r != "Metadata",
|
||||
);
|
||||
|
||||
t.pass(
|
||||
m.new_test_case()
|
||||
.name("stripped_metadata_doesnt_validate_against_different")
|
||||
.validation_metadata(validation_metadata)
|
||||
.expects_invalid()
|
||||
.build(codegen_metadata),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_fail() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("src/incorrect/*.rs");
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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 frame_metadata::{
|
||||
v15::{RuntimeApiMetadata, RuntimeApiMethodMetadata, RuntimeApiMethodParamMetadata},
|
||||
RuntimeMetadataPrefixed,
|
||||
};
|
||||
|
||||
use crate::utils::generate_metadata_from_runtime_apis;
|
||||
|
||||
/// Generate metadata which contains a `Map` storage entry with no hashers/values.
|
||||
/// This is a bit of an odd case, but it was raised in https://github.com/paritytech/subxt/issues/552,
|
||||
/// and this test will fail before the fix and should pass once the fix is applied.
|
||||
pub fn metadata_runtime_api_underscore_method_name() -> RuntimeMetadataPrefixed {
|
||||
generate_metadata_from_runtime_apis(vec![RuntimeApiMetadata {
|
||||
name: "MyApi".to_owned(),
|
||||
docs: vec![],
|
||||
methods: vec![RuntimeApiMethodMetadata {
|
||||
name: "my_method".to_owned(),
|
||||
inputs: vec![RuntimeApiMethodParamMetadata {
|
||||
name: "_".to_owned(), // The important bit we're testing.
|
||||
ty: 0.into(), // we don't care what type this is.
|
||||
}],
|
||||
output: 0.into(), // we don't care what type this is.
|
||||
docs: vec![],
|
||||
}],
|
||||
}])
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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 frame_metadata::{
|
||||
v15::{StorageEntryMetadata, StorageEntryModifier, StorageEntryType},
|
||||
RuntimeMetadataPrefixed,
|
||||
};
|
||||
use scale_info::meta_type;
|
||||
|
||||
use crate::utils::generate_metadata_from_storage_entries;
|
||||
|
||||
/// Generate metadata which contains a `Map` storage entry with no hashers/values.
|
||||
/// This is a bit of an odd case, but it was raised in https://github.com/paritytech/subxt/issues/552,
|
||||
/// and this test will fail before the fix and should pass once the fix is applied.
|
||||
pub fn metadata_storage_map_no_keys() -> RuntimeMetadataPrefixed {
|
||||
generate_metadata_from_storage_entries(vec![StorageEntryMetadata {
|
||||
name: "MapWithNoKeys",
|
||||
modifier: StorageEntryModifier::Optional,
|
||||
ty: StorageEntryType::Map {
|
||||
hashers: vec![],
|
||||
key: meta_type::<()>(),
|
||||
value: meta_type::<u32>(),
|
||||
},
|
||||
default: vec![0],
|
||||
docs: vec![],
|
||||
}])
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// 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, Encode};
|
||||
use frame_metadata::RuntimeMetadataPrefixed;
|
||||
use std::io::Read;
|
||||
|
||||
static TEST_DIR_PREFIX: &str = "subxt_generated_ui_tests_";
|
||||
static METADATA_FILE: &str = "../../artifacts/polkadot_metadata_full.scale";
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MetadataTestRunner {
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl MetadataTestRunner {
|
||||
/// Loads metadata that we can use in our tests. Panics if
|
||||
/// there is some issue decoding the metadata.
|
||||
pub fn load_metadata() -> RuntimeMetadataPrefixed {
|
||||
let mut file =
|
||||
std::fs::File::open(METADATA_FILE).expect("Cannot open metadata.scale artifact");
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
file.read_to_end(&mut bytes)
|
||||
.expect("Failed to read metadata.scale file");
|
||||
|
||||
RuntimeMetadataPrefixed::decode(&mut &*bytes).expect("Cannot decode metadata bytes")
|
||||
}
|
||||
|
||||
/// Create a new test case.
|
||||
pub fn new_test_case(&mut self) -> MetadataTestRunnerCaseBuilder {
|
||||
let index = self.index;
|
||||
// increment index so that each test case gets its own folder path.
|
||||
self.index += 1;
|
||||
|
||||
MetadataTestRunnerCaseBuilder::new(index)
|
||||
}
|
||||
}
|
||||
|
||||
// `trybuild` runs all tests once it's dropped. So, we defer all cleanup until we
|
||||
// are dropped too, to make sure that cleanup happens after tests are ran.
|
||||
impl Drop for MetadataTestRunner {
|
||||
fn drop(&mut self) {
|
||||
for i in 0..self.index {
|
||||
let mut tmp_dir = std::env::temp_dir();
|
||||
tmp_dir.push(format!("{TEST_DIR_PREFIX}{i}"));
|
||||
std::fs::remove_dir_all(tmp_dir).expect("cannot cleanup temp files");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a single test case.
|
||||
pub struct MetadataTestRunnerCaseBuilder {
|
||||
index: usize,
|
||||
name: String,
|
||||
validation_metadata: Option<RuntimeMetadataPrefixed>,
|
||||
should_be_valid: bool,
|
||||
}
|
||||
|
||||
impl MetadataTestRunnerCaseBuilder {
|
||||
fn new(index: usize) -> Self {
|
||||
MetadataTestRunnerCaseBuilder {
|
||||
index,
|
||||
name: format!("Test {index}"),
|
||||
validation_metadata: None,
|
||||
should_be_valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the test name.
|
||||
pub fn name(mut self, name: impl AsRef<str>) -> Self {
|
||||
name.as_ref().clone_into(&mut self.name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set metadata to be validated against the generated code.
|
||||
/// By default, we'll validate the same metadata used to generate the code.
|
||||
pub fn validation_metadata(mut self, md: impl Into<RuntimeMetadataPrefixed>) -> Self {
|
||||
self.validation_metadata = Some(md.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Expect the validation metadata provided to _not_ be valid.
|
||||
pub fn expects_invalid(mut self) -> Self {
|
||||
self.should_be_valid = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// At the minimum, takes some metadata and a test name, generates the code
|
||||
/// and hands back a path to some generated code that `trybuild` can be pointed at.
|
||||
/// validation metadata and expected validity can also be provided.
|
||||
///
|
||||
/// The generated code:
|
||||
/// - checks that the subxt macro can perform codegen given the
|
||||
/// provided macro_metadata without running into any issues.
|
||||
/// - checks that the `runtime::is_codegen_valid_for` function returns
|
||||
/// true or false when compared to the `validation_metadata`, according
|
||||
/// to whether `expects_invalid()` is set or not.
|
||||
///
|
||||
/// The generated code will be tidied up when the `MetadataTestRunner` that
|
||||
/// this was handed out from is dropped.
|
||||
pub fn build(self, macro_metadata: frame_metadata::RuntimeMetadataPrefixed) -> String {
|
||||
let validation_metadata = self.validation_metadata.unwrap_or_else(|| {
|
||||
// RuntimeMetadataPrefixed doesn't implement Clone for some reason (we should prob fix that).
|
||||
// until then, this hack clones it by encoding and then decoding it again from bytes..
|
||||
clone_via_encode(¯o_metadata)
|
||||
});
|
||||
|
||||
let index = self.index;
|
||||
let mut tmp_dir = std::env::temp_dir();
|
||||
tmp_dir.push(format!("{TEST_DIR_PREFIX}{index}"));
|
||||
|
||||
let tmp_macro_metadata_path = {
|
||||
let mut t = tmp_dir.clone();
|
||||
t.push("macro_metadata.scale");
|
||||
t.to_string_lossy().into_owned()
|
||||
};
|
||||
let tmp_validation_metadata_path = {
|
||||
let mut t = tmp_dir.clone();
|
||||
t.push("validation_metadata.scale");
|
||||
t.to_string_lossy().into_owned()
|
||||
};
|
||||
let tmp_rust_path = {
|
||||
let mut t = tmp_dir.clone();
|
||||
let test_name = &self.name;
|
||||
t.push(format!("{test_name}.rs"));
|
||||
t.to_string_lossy().into_owned()
|
||||
};
|
||||
|
||||
let encoded_macro_metadata = macro_metadata.encode();
|
||||
let encoded_validation_metadata = validation_metadata.encode();
|
||||
|
||||
let should_be_valid_str = if self.should_be_valid {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
};
|
||||
|
||||
let rust_file = format!(
|
||||
r#"
|
||||
use subxt;
|
||||
use subxt::ext::codec::Decode;
|
||||
use std::io::Read;
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "{tmp_macro_metadata_path}")]
|
||||
pub mod polkadot {{}}
|
||||
|
||||
fn main() {{
|
||||
// load validation metadata:
|
||||
let mut file = std::fs::File::open("{tmp_validation_metadata_path}")
|
||||
.expect("validation_metadata exists");
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
file.read_to_end(&mut bytes)
|
||||
.expect("Failed to read metadata.scale file");
|
||||
|
||||
let metadata = subxt::Metadata::decode(&mut &*bytes)
|
||||
.expect("Cannot decode metadata bytes");
|
||||
|
||||
// validate it:
|
||||
let is_valid = polkadot::is_codegen_valid_for(&metadata);
|
||||
assert_eq!(is_valid, {should_be_valid_str}, "expected validity to line up");
|
||||
}}
|
||||
"#
|
||||
);
|
||||
|
||||
std::fs::create_dir_all(&tmp_dir).expect("could not create tmp ui test dir");
|
||||
// Write metadatas to tmp folder:
|
||||
std::fs::write(&tmp_macro_metadata_path, encoded_macro_metadata).unwrap();
|
||||
std::fs::write(&tmp_validation_metadata_path, encoded_validation_metadata).unwrap();
|
||||
// Write test file to tmp folder (it'll be moved by trybuild):
|
||||
std::fs::write(&tmp_rust_path, rust_file).unwrap();
|
||||
|
||||
tmp_rust_path
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_via_encode<T: codec::Encode + codec::Decode>(item: &T) -> T {
|
||||
let bytes = item.encode();
|
||||
T::decode(&mut &*bytes).unwrap()
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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 metadata_test_runner;
|
||||
|
||||
use frame_metadata::{
|
||||
v15::{
|
||||
CustomMetadata, ExtrinsicMetadata, OuterEnums, PalletMetadata, PalletStorageMetadata,
|
||||
RuntimeApiMetadata, RuntimeMetadataV15, StorageEntryMetadata,
|
||||
},
|
||||
RuntimeMetadataPrefixed,
|
||||
};
|
||||
use generate_custom_metadata::dispatch_error::ArrayDispatchError;
|
||||
use scale_info::{form::PortableForm, meta_type, IntoPortable, TypeInfo};
|
||||
|
||||
pub use metadata_test_runner::MetadataTestRunner;
|
||||
|
||||
/// Given some pallet metadata, generate a [`RuntimeMetadataPrefixed`] struct.
|
||||
/// We default to a useless extrinsic type, and register a fake `DispatchError`
|
||||
/// type matching the generic type param provided.
|
||||
pub fn generate_metadata_from_pallets_custom_dispatch_error<DispatchError: TypeInfo + 'static>(
|
||||
pallets: Vec<PalletMetadata>,
|
||||
runtime_apis: Vec<RuntimeApiMetadata<PortableForm>>,
|
||||
) -> RuntimeMetadataPrefixed {
|
||||
// We don't care about the extrinsic type.
|
||||
let extrinsic = ExtrinsicMetadata {
|
||||
version: 0,
|
||||
signed_extensions: vec![],
|
||||
address_ty: meta_type::<()>(),
|
||||
call_ty: meta_type::<()>(),
|
||||
signature_ty: meta_type::<()>(),
|
||||
extra_ty: meta_type::<()>(),
|
||||
};
|
||||
|
||||
// Construct metadata manually from our types (See `RuntimeMetadataV15::new()`).
|
||||
// Add any extra types we need to the registry.
|
||||
let mut registry = scale_info::Registry::new();
|
||||
let pallets = registry.map_into_portable(pallets);
|
||||
let extrinsic = extrinsic.into_portable(&mut registry);
|
||||
|
||||
#[derive(TypeInfo)]
|
||||
struct Runtime;
|
||||
#[derive(TypeInfo)]
|
||||
enum RuntimeCall {}
|
||||
#[derive(TypeInfo)]
|
||||
enum RuntimeEvent {}
|
||||
#[derive(TypeInfo)]
|
||||
enum RuntimeError {}
|
||||
|
||||
let ty = registry.register_type(&meta_type::<Runtime>());
|
||||
let runtime_call = registry.register_type(&meta_type::<RuntimeCall>());
|
||||
let runtime_event = registry.register_type(&meta_type::<RuntimeEvent>());
|
||||
let runtime_error = registry.register_type(&meta_type::<RuntimeError>());
|
||||
|
||||
// Metadata needs to contain this DispatchError, since codegen looks for it.
|
||||
registry.register_type(&meta_type::<DispatchError>());
|
||||
|
||||
let metadata = RuntimeMetadataV15 {
|
||||
types: registry.into(),
|
||||
pallets,
|
||||
extrinsic,
|
||||
ty,
|
||||
apis: runtime_apis,
|
||||
outer_enums: OuterEnums {
|
||||
call_enum_ty: runtime_call,
|
||||
event_enum_ty: runtime_event,
|
||||
error_enum_ty: runtime_error,
|
||||
},
|
||||
custom: CustomMetadata {
|
||||
map: Default::default(),
|
||||
},
|
||||
};
|
||||
|
||||
RuntimeMetadataPrefixed::from(metadata)
|
||||
}
|
||||
|
||||
/// Given some pallet metadata, generate a [`RuntimeMetadataPrefixed`] struct.
|
||||
/// We default to a useless extrinsic type, and register a fake `DispatchError`
|
||||
/// type so that codegen is happy with the metadata generated.
|
||||
pub fn generate_metadata_from_pallets(pallets: Vec<PalletMetadata>) -> RuntimeMetadataPrefixed {
|
||||
generate_metadata_from_pallets_custom_dispatch_error::<ArrayDispatchError>(pallets, vec![])
|
||||
}
|
||||
|
||||
/// Given some runtime API metadata, generate a [`RuntimeMetadataPrefixed`] struct.
|
||||
/// We default to a useless extrinsic type, and register a fake `DispatchError`
|
||||
/// type so that codegen is happy with the metadata generated.
|
||||
pub fn generate_metadata_from_runtime_apis(
|
||||
runtime_apis: Vec<RuntimeApiMetadata<PortableForm>>,
|
||||
) -> RuntimeMetadataPrefixed {
|
||||
generate_metadata_from_pallets_custom_dispatch_error::<ArrayDispatchError>(vec![], runtime_apis)
|
||||
}
|
||||
|
||||
/// Given some storage entries, generate a [`RuntimeMetadataPrefixed`] struct.
|
||||
/// We default to a useless extrinsic type, mock a pallet out, and register a
|
||||
/// fake `DispatchError` type so that codegen is happy with the metadata generated.
|
||||
pub fn generate_metadata_from_storage_entries(
|
||||
storage_entries: Vec<StorageEntryMetadata>,
|
||||
) -> RuntimeMetadataPrefixed {
|
||||
let storage = PalletStorageMetadata {
|
||||
prefix: "System",
|
||||
entries: storage_entries,
|
||||
};
|
||||
|
||||
let pallet = PalletMetadata {
|
||||
index: 0,
|
||||
name: "System",
|
||||
storage: Some(storage),
|
||||
constants: vec![],
|
||||
calls: None,
|
||||
event: None,
|
||||
error: None,
|
||||
docs: vec![],
|
||||
};
|
||||
|
||||
generate_metadata_from_pallets(vec![pallet])
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
+2807
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "wasm-lightclient-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.24"
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing = "0.1.34"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
serde_json = "1"
|
||||
futures-util = "0.3.30"
|
||||
|
||||
# This crate is not a part of the workspace, because it
|
||||
# requires the "jsonrpsee web unstable-light-client" features to be enabled, which we don't
|
||||
# want enabled for workspace builds in general.
|
||||
subxt = { path = "../../subxt", default-features = false, features = ["web", "jsonrpsee", "unstable-light-client"] }
|
||||
@@ -0,0 +1,76 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use subxt::{client::OnlineClient, config::PolkadotConfig, lightclient::LightClient};
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// Run the tests by calling:
|
||||
//
|
||||
// ```text
|
||||
// wasm-pack test --firefox --headless
|
||||
// ```
|
||||
//
|
||||
// Use the following to enable logs:
|
||||
// ```
|
||||
// console_error_panic_hook::set_once();
|
||||
// tracing_wasm::set_as_global_default();
|
||||
// ```
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn light_client_works() {
|
||||
let api = connect_to_rpc_node().await;
|
||||
|
||||
tracing::info!("Subscribe to latest finalized blocks: ");
|
||||
|
||||
let mut blocks_sub = api
|
||||
.blocks()
|
||||
.subscribe_finalized()
|
||||
.await
|
||||
.expect("Cannot subscribe to finalized hashes")
|
||||
.take(3);
|
||||
|
||||
// For each block, print information about it:
|
||||
while let Some(block) = blocks_sub.next().await {
|
||||
let block = block.expect("Block not valid");
|
||||
|
||||
let block_number = block.header().number;
|
||||
let block_hash = block.hash();
|
||||
|
||||
tracing::info!("Block #{block_number}:");
|
||||
tracing::info!(" Hash: {block_hash}");
|
||||
}
|
||||
}
|
||||
|
||||
/// We connect to an RPC node because the light client can struggle to sync in
|
||||
/// time to a new local node for some reason. Because this can be brittle (eg RPC nodes can
|
||||
/// go down or have network issues), we try a few RPC nodes until we find one that works.
|
||||
async fn connect_to_rpc_node() -> OnlineClient<PolkadotConfig> {
|
||||
let rpc_node_urls = [
|
||||
"wss://rpc.polkadot.io",
|
||||
"wss://1rpc.io/dot",
|
||||
"wss://polkadot-public-rpc.blockops.network/ws",
|
||||
];
|
||||
|
||||
async fn do_connect(
|
||||
url: &str,
|
||||
) -> Result<OnlineClient<PolkadotConfig>, Box<dyn std::error::Error + Send + Sync + 'static>>
|
||||
{
|
||||
let chainspec = subxt::utils::fetch_chainspec_from_rpc_node(url).await?;
|
||||
let (_lc, rpc) = LightClient::relay_chain(chainspec.get())?;
|
||||
let api = OnlineClient::from_rpc_client(rpc).await?;
|
||||
Ok(api)
|
||||
}
|
||||
|
||||
for url in rpc_node_urls {
|
||||
let res = do_connect(url).await;
|
||||
|
||||
match res {
|
||||
Ok(api) => return api,
|
||||
Err(e) => tracing::warn!("Error connecting to RPC node {url}: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Could not connect to any RPC node")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
+2808
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "wasm-rpc-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.24"
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing = "0.1.34"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
serde_json = "1"
|
||||
futures-util = "0.3.30"
|
||||
|
||||
# This crate is not a part of the workspace, because it
|
||||
# requires the "jsonrpsee web" features to be enabled, which we don't
|
||||
# want enabled for workspace builds in general.
|
||||
subxt = { path = "../../subxt", default-features = false, features = ["web", "jsonrpsee", "reconnecting-rpc-client"] }
|
||||
@@ -0,0 +1,60 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use subxt::config::SubstrateConfig;
|
||||
use subxt::backend::rpc::reconnecting_rpc_client::RpcClient as ReconnectingRpcClient;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// Run the tests by calling:
|
||||
//
|
||||
// ```text
|
||||
// wasm-pack test --firefox --headless`
|
||||
// ```
|
||||
//
|
||||
// You'll need to have a substrate node running:
|
||||
//
|
||||
// ```bash
|
||||
// ./substrate-node --dev --node-key 0000000000000000000000000000000000000000000000000000000000000001 --listen-addr /ip4/0.0.0.0/tcp/30333/ws
|
||||
// ```
|
||||
//
|
||||
// Use the following to enable logs:
|
||||
// ```
|
||||
// console_error_panic_hook::set_once();
|
||||
// tracing_wasm::set_as_global_default();
|
||||
// ```
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn wasm_ws_transport_works() {
|
||||
console_error_panic_hook::set_once();
|
||||
tracing_wasm::set_as_global_default();
|
||||
let client = subxt::client::OnlineClient::<SubstrateConfig>::from_url("ws://127.0.0.1:9944")
|
||||
.await
|
||||
.unwrap();
|
||||
let hasher = client.hasher();
|
||||
|
||||
let mut stream = client.backend().stream_best_block_headers(hasher).await.unwrap();
|
||||
assert!(stream.next().await.is_some());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn wasm_ws_chainhead_works() {
|
||||
let rpc = subxt::backend::rpc::RpcClient::from_url("ws://127.0.0.1:9944").await.unwrap();
|
||||
let backend = subxt::backend::chain_head::ChainHeadBackendBuilder::new().build_with_background_driver(rpc);
|
||||
let client = subxt::client::OnlineClient::<SubstrateConfig>::from_backend(std::sync::Arc::new(backend)).await.unwrap();
|
||||
let hasher = client.hasher();
|
||||
|
||||
let mut stream = client.backend().stream_best_block_headers(hasher).await.unwrap();
|
||||
assert!(stream.next().await.is_some());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn reconnecting_rpc_client_ws_transport_works() {
|
||||
let rpc = ReconnectingRpcClient::builder().build("ws://127.0.0.1:9944".to_string()).await.unwrap();
|
||||
let client = subxt::client::OnlineClient::<SubstrateConfig>::from_rpc_client(rpc.clone()).await.unwrap();
|
||||
let hasher = client.hasher();
|
||||
|
||||
let mut stream = client.backend().stream_best_block_headers(hasher).await.unwrap();
|
||||
assert!(stream.next().await.is_some());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user