fix: Convert vendor/pezkuwi-subxt from submodule to regular directory
This commit is contained in:
@@ -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() => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user