// 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 pezkuwi_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<(), pezkuwi_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<(), pezkuwi_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/pezkuwichain/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::().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_pezkuwijs() { 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 bizinikiwi node. // - open pezkuwi.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 pezkuwi 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 #[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, /// tip pub tip: u128, } /// taken from original type /// 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::( "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/pezkuwichain/subxt/actions/runs/13374953009/job/37353887719?pr=1910#step:7:178 // https://github.com/pezkuwichain/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::>().await; (blocks, missed_blocks) } }; let (blocks, _) = chainhead_client_blocks(3).await; let blocks: HashSet = 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 = HashSet::from_iter(unstable_blocks.into_iter()); let intersection = unstable_blocks.intersection(&blocks).count(); assert!(intersection >= 3, "intersections size is {}", intersection); } } */