// 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 pezkuwi_subxt::{ config::{ DefaultExtrinsicParamsBuilder, BizinikiwConfig, 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<(), pezkuwi_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(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<(), pezkuwi_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<(), pezkuwi_subxt::Error> { use pezkuwi_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 pezkuwi_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<(), pezkuwi_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, 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::() .unwrap() ); // `.find_first` should similarly work to find the transfer call: assert!( extrinsics .find_first::() .unwrap() .is_some() ); let block_extrinsics = extrinsics.iter().collect::>(); let mut balance = None; let mut timestamp = None; for tx in block_extrinsics { tx.as_root_extrinsic::().unwrap(); if let Some(ext) = tx .as_extrinsic::() .unwrap() { timestamp = Some((ext, tx.is_signed())); } if let Some(ext) = tx .as_extrinsic::() .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: &pezkuwi_subxt::OnlineClient, params: pezkuwi_subxt::config::DefaultExtrinsicParamsBuilder, ) -> pezkuwi_subxt::blocks::ExtrinsicDetails> { 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::().unwrap().unwrap(); let tip1 = extensions1.tip().unwrap(); let tip1_static: u128 = extensions1 .find::>() .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::().unwrap().unwrap(); let tip2 = extensions2.tip().unwrap(); let tip2_static: u128 = extensions2 .find::>() .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::>() .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::>() .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::>() .unwrap() .unwrap(); assert!(matches!( mortality, Era::Mortal { period: 32, phase: _, // depends on current block so don't test it. } )); } }