mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 00:01:09 +00:00
7af0bcd262
- Replace PolkadotConfig with PezkuwiConfig - Replace SubstrateConfig with BizinikiwConfig - Rename config module files (polkadot.rs→pezkuwi.rs, substrate.rs→bizinikiwi.rs) - Update all documentation and examples - All 165 files updated, cargo check passes
456 lines
14 KiB
Rust
456 lines
14 KiB
Rust
// 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::<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_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 <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/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::<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);
|
|
}
|
|
}
|
|
*/
|