// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Test the eth-rpc cli with the kitchensink node. //! This only includes basic transaction tests, most of the other tests are in the //! [evm-test-suite](https://github.com/paritytech/evm-test-suite) repository. use crate::{ cli::{self, CliCommand}, client, example::TransactionBuilder, subxt_client::{ self, src_chain::runtime_types::pezpallet_revive::primitives::Code, SrcChainConfig, }, EthRpcClient, }; use anyhow::anyhow; use clap::Parser; use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; use pezpallet_revive::{ create1, evm::{ Account, Block, BlockNumberOrTag, BlockNumberOrTagOrHash, BlockTag, HashesOrTransactionInfos, TransactionInfo, H256, U256, }, }; use std::{sync::Arc, thread}; use subxt::{ backend::rpc::RpcClient, ext::subxt_rpcs::rpc_params, tx::{SubmittableTransaction, TxStatus}, OnlineClient, }; const LOG_TARGET: &str = "eth-rpc-tests"; /// Create a websocket client with a 120s timeout. async fn ws_client_with_retry(url: &str) -> WsClient { let timeout = tokio::time::Duration::from_secs(120); tokio::time::timeout(timeout, async { loop { if let Ok(client) = WsClientBuilder::default().build(url).await { return client; } else { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } } }) .await .expect("Hit timeout") } struct SharedResources { _node_handle: std::thread::JoinHandle<()>, _rpc_handle: std::thread::JoinHandle<()>, } impl SharedResources { fn start() -> Self { // Start pez-revive-dev-node let _node_handle = thread::spawn(move || { if let Err(e) = pez_revive_dev_node::command::run_with_args(vec![ "--dev".to_string(), "--rpc-port=45789".to_string(), "-lerror,pezsc_rpc_server=info,runtime::revive=debug".to_string(), ]) { panic!("Node exited with error: {e:?}"); } }); // Start the rpc server. let args = CliCommand::parse_from([ "--dev", "--rpc-port=45788", "--pez-node-rpc-url=ws://localhost:45789", "--no-prometheus", "-linfo,eth-rpc=debug", ]); let _rpc_handle = thread::spawn(move || { if let Err(e) = cli::run(args) { panic!("eth-rpc exited with error: {e:?}"); } }); Self { _node_handle, _rpc_handle } } async fn client() -> WsClient { ws_client_with_retry("ws://localhost:45788").await } fn pez_node_rpc_url() -> &'static str { "ws://localhost:45789" } } macro_rules! unwrap_call_err( ($err:expr) => { match $err.downcast_ref::().unwrap() { jsonrpsee::core::client::Error::Call(call) => call, _ => panic!("Expected Call error"), } } ); // Helper functions /// Prepare multiple EVM transfer transactions with nonce in descending order async fn prepare_evm_transactions( client: &Arc, signer: Account, recipient: pezpallet_revive::evm::Address, amount: U256, count: usize, ) -> anyhow::Result>> { let start_nonce = client.get_transaction_count(signer.address(), BlockTag::Latest.into()).await?; let mut transactions = Vec::new(); for i in (0..count).rev() { let nonce = start_nonce.saturating_add(U256::from(i as u64)); let tx_builder = TransactionBuilder::new(client) .signer(signer.clone()) .nonce(nonce) .value(amount) .to(recipient); transactions.push(tx_builder); log::trace!(target: LOG_TARGET, "Prepared EVM transaction {}/{count} with nonce: {nonce:?}", i + 1); } Ok(transactions) } /// Prepare multiple Bizinikiwi transfer transactions with sequential nonces async fn prepare_bizinikiwi_transactions( node_client: &OnlineClient, signer: &subxt_signer::sr25519::Keypair, count: usize, ) -> anyhow::Result>>> { let mut nonce = node_client.tx().account_nonce(&signer.public_key().into()).await?; let mut bizinikiwi_txs = Vec::new(); for i in 0..count { let remark_data = format!("Hello from test {}", i); let call = subxt::dynamic::tx( "System", "remark", vec![subxt::dynamic::Value::from_bytes(remark_data.as_bytes())], ); // Note: Using polkadot config from subxt (external crate) let params = subxt::config::polkadot::PolkadotExtrinsicParamsBuilder::new() .nonce(nonce) .build(); let tx = node_client.tx().create_signed(&call, signer, params).await?; bizinikiwi_txs.push(tx); log::trace!(target: LOG_TARGET, "Prepared bizinikiwi transaction {i}/{count} with nonce: {nonce}"); nonce += 1 as u64; } Ok(bizinikiwi_txs) } /// Submit multiple transactions and return them without waiting for receipts async fn submit_evm_transactions( transactions: Vec>, ) -> anyhow::Result< Vec<( H256, pezpallet_revive::evm::GenericTransaction, crate::example::SubmittedTransaction, )>, > { let mut submitted_txs = Vec::new(); for tx_builder in transactions { let tx = tx_builder.send().await?; let hash = tx.hash(); let generic_tx = tx.generic_transaction(); submitted_txs.push((hash, generic_tx, tx)); } Ok(submitted_txs) } /// Submit bizinikiwi transactions and return futures for waiting async fn submit_bizinikiwi_transactions( bizinikiwi_txs: Vec>>, ) -> Vec>> { let mut futures = Vec::new(); for (i, tx) in bizinikiwi_txs.into_iter().enumerate() { let fut = async move { match tx.submit_and_watch().await { Ok(mut progress) => { log::trace!(target: LOG_TARGET, "Bizinikiwi tx {i} submitted"); while let Some(status) = progress.next().await { match status { Ok(TxStatus::InFinalizedBlock(block)) | Ok(TxStatus::InBestBlock(block)) => { log::trace!(target: LOG_TARGET, "Bizinikiwi tx {i} included in block {:?}", block.block_hash() ); return Ok(()); }, Err(e) => return Err(anyhow::anyhow!("Bizinikiwi tx {i} error: {e}")), Ok(status) => { log::trace!(target: LOG_TARGET, "Bizinikiwi tx {i} status {:?}", status); }, } } Err(anyhow::anyhow!( "Failed to get status of submitted bizinikiwi tx {i}, assuming error" )) }, Err(e) => Err(anyhow::anyhow!("Failed to submit bizinikiwi tx {i}: {e}")), } }; futures.push(fut); } futures } /// Verify all given transaction hashes are in the specified block and accessible via RPC async fn verify_transactions_in_single_block( client: &Arc, block_number: U256, expected_tx_hashes: &[H256], ) -> anyhow::Result<()> { // Fetch the block let block = client .get_block_by_number(BlockNumberOrTag::U256(block_number), false) .await? .ok_or_else(|| anyhow!("Block {block_number} should exist"))?; let block_tx_hashes = match &block.transactions { HashesOrTransactionInfos::Hashes(hashes) => hashes.clone(), HashesOrTransactionInfos::TransactionInfos(infos) => infos.iter().map(|info| info.hash).collect(), }; if let Some(missing_hash) = expected_tx_hashes.iter().find(|hash| !block_tx_hashes.contains(hash)) { return Err(anyhow!("Transaction {missing_hash:?} not found in block {block_number}")); } Ok(()) } #[tokio::test] async fn run_all_eth_rpc_tests() -> anyhow::Result<()> { // start node and rpc server let _shared = SharedResources::start(); let client = Arc::new(SharedResources::client().await); macro_rules! run_tests { ($($test:ident),+ $(,)?) => { $( { let test_name = stringify!($test); log::debug!(target: LOG_TARGET, "Running test: {}", test_name); match $test(client.clone()).await { Ok(()) => log::debug!(target: LOG_TARGET, "Test passed: {}", test_name), Err(err) => panic!("Test {} failed: {err:?}", test_name), } } )+ }; } run_tests!( test_transfer, test_deploy_and_call, test_runtime_api_dry_run_addr_works, test_invalid_transaction, test_evm_blocks_should_match, test_evm_blocks_hydrated_should_match, test_block_hash_for_tag_with_proper_ethereum_block_hash_works, test_block_hash_for_tag_with_invalid_ethereum_block_hash_fails, test_block_hash_for_tag_with_block_number_works, test_block_hash_for_tag_with_block_tags_works, test_multiple_transactions_in_block, test_mixed_evm_bizinikiwi_transactions, test_runtime_pallets_address_upload_code, ); log::debug!(target: LOG_TARGET, "All tests completed successfully!"); Ok(()) } async fn test_transfer(client: Arc) -> anyhow::Result<()> { let ethan = Account::from(subxt_signer::eth::dev::ethan()); let initial_balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?; let value = 1_000_000_000_000_000_000_000u128.into(); let tx = TransactionBuilder::new(&client).value(value).to(ethan.address()).send().await?; let receipt = tx.wait_for_receipt().await?; assert_eq!( Some(ethan.address()), receipt.to, "Receipt should have the correct contract address." ); let balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?; assert_eq!( Some(value), balance.checked_sub(initial_balance), "Ethan {:?} {balance:?} should have increased by {value:?} from {initial_balance}.", ethan.address() ); Ok(()) } async fn test_deploy_and_call(client: Arc) -> anyhow::Result<()> { let account = Account::default(); // Balance transfer let ethan = Account::from(subxt_signer::eth::dev::ethan()); let initial_balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?; let value = 1_000_000_000_000_000_000_000u128.into(); let tx = TransactionBuilder::new(&client).value(value).to(ethan.address()).send().await?; let receipt = tx.wait_for_receipt().await?; assert_eq!( Some(ethan.address()), receipt.to, "Receipt should have the correct contract address." ); let balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?; assert_eq!( Some(value), balance.checked_sub(initial_balance), "Ethan {:?} {balance:?} should have increased by {value:?} from {initial_balance}.", ethan.address() ); // Deploy contract let data = b"hello world".to_vec(); let value = U256::from(5_000_000_000_000u128); let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?; let input = bytes.into_iter().chain(data.clone()).collect::>(); let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?; let tx = TransactionBuilder::new(&client).value(value).input(input).send().await?; let receipt = tx.wait_for_receipt().await?; let contract_address = create1(&account.address(), nonce.try_into().unwrap()); assert_eq!( Some(contract_address), receipt.contract_address, "Contract should be deployed at {contract_address:?}." ); let nonce_after_deploy = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?; assert_eq!(nonce_after_deploy - nonce, U256::from(1), "Nonce should have increased by 1"); let initial_balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?; assert_eq!( value, initial_balance, "Contract {contract_address:?} balance should be the same as the value sent ({value})." ); // Call contract let tx = TransactionBuilder::new(&client) .value(value) .to(contract_address) .send() .await?; let receipt = tx.wait_for_receipt().await?; assert_eq!( Some(contract_address), receipt.to, "Receipt should have the correct contract address {contract_address:?}." ); let balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?; assert_eq!(Some(value), balance.checked_sub(initial_balance), "Contract {contract_address:?} Balance {balance} should have increased from {initial_balance} by {value}."); // Balance transfer to contract let initial_balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?; let tx = TransactionBuilder::new(&client) .value(value) .to(contract_address) .send() .await?; tx.wait_for_receipt().await?; let balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?; assert_eq!( Some(value), balance.checked_sub(initial_balance), "Balance {balance} should have increased from {initial_balance} by {value}." ); Ok(()) } async fn test_runtime_api_dry_run_addr_works(client: Arc) -> anyhow::Result<()> { let account = Account::default(); let origin: [u8; 32] = account.bizinikiwi_account().into(); let data = b"hello world".to_vec(); let value = 5_000_000_000_000u128; let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?; let payload = subxt_client::apis().revive_api().instantiate( subxt::utils::AccountId32(origin), value, None, None, Code::Upload(bytes), data, None, ); // runtime_api.at_latest() uses the latest finalized block, query nonce accordingly let nonce = client .get_transaction_count(account.address(), BlockTag::Finalized.into()) .await?; let contract_address = create1(&account.address(), nonce.try_into().unwrap()); let c = OnlineClient::::from_url("ws://localhost:45789").await?; let res = c.runtime_api().at_latest().await?.call(payload).await?.result.unwrap(); assert_eq!(res.addr, contract_address); Ok(()) } async fn test_invalid_transaction(client: Arc) -> anyhow::Result<()> { let ethan = Account::from(subxt_signer::eth::dev::ethan()); let err = TransactionBuilder::new(&client) .value(U256::from(1_000_000_000_000u128)) .to(ethan.address()) .mutate(|tx| tx.chain_id = Some(42u32.into())) .send() .await .unwrap_err(); let call_err = unwrap_call_err!(err.source().unwrap()); assert_eq!(call_err.message(), "Invalid Transaction"); Ok(()) } async fn get_evm_block_from_storage( node_client: &OnlineClient, pez_node_rpc_client: &RpcClient, block_number: U256, ) -> anyhow::Result { let block_hash: H256 = pez_node_rpc_client .request("chain_getBlockHash", rpc_params![block_number]) .await .unwrap(); let query = subxt_client::storage().revive().ethereum_block(); let Some(block) = node_client.storage().at(block_hash).fetch(&query).await.unwrap() else { return Err(anyhow!("EVM block {block_hash:?} not found")); }; Ok(block.0) } async fn test_evm_blocks_should_match(client: Arc) -> anyhow::Result<()> { let (node_client, pez_node_rpc_client, _) = client::connect(SharedResources::pez_node_rpc_url()).await.unwrap(); // Deploy a contract to have some interesting blocks let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?; let value = U256::from(5_000_000_000_000u128); let tx = TransactionBuilder::new(&client) .value(value) .input(bytes.to_vec()) .send() .await?; let receipt = tx.wait_for_receipt().await?; let block_number = receipt.block_number; let block_hash = receipt.block_hash; log::trace!(target: LOG_TARGET, "block_number = {block_number:?}"); log::trace!(target: LOG_TARGET, "tx hash = {:?}", tx.hash()); let evm_block_from_storage = get_evm_block_from_storage(&node_client, &pez_node_rpc_client, block_number).await?; // Fetch the block immediately (should come from storage EthereumBlock) let evm_block_from_rpc_by_number = client .get_block_by_number(BlockNumberOrTag::U256(block_number.into()), false) .await? .expect("Block should exist"); let evm_block_from_rpc_by_hash = client.get_block_by_hash(block_hash, false).await?.expect("Block should exist"); assert!( matches!( evm_block_from_rpc_by_number.transactions, pezpallet_revive::evm::HashesOrTransactionInfos::Hashes(_) ), "Block should not have hydrated transactions" ); // All EVM blocks must match assert_eq!(evm_block_from_storage, evm_block_from_rpc_by_number, "EVM blocks should match"); assert_eq!(evm_block_from_storage, evm_block_from_rpc_by_hash, "EVM blocks should match"); Ok(()) } async fn test_evm_blocks_hydrated_should_match(client: Arc) -> anyhow::Result<()> { // Deploy a contract to have some transactions in the block let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?; let value = U256::from(5_000_000_000_000u128); let signer = Account::default(); let signer_copy = Account::default(); let tx = TransactionBuilder::new(&client) .value(value) .signer(signer) .input(bytes.to_vec()) .send() .await?; let receipt = tx.wait_for_receipt().await?; let block_number = receipt.block_number; let block_hash = receipt.block_hash; log::trace!(target: LOG_TARGET, "block_number = {block_number:?}"); log::trace!(target: LOG_TARGET, "tx hash = {:?}", tx.hash()); // Fetch the block with hydrated transactions via RPC (by number and by hash) let evm_block_from_rpc_by_number = client .get_block_by_number(BlockNumberOrTag::U256(block_number.into()), true) .await? .expect("Block should exist"); let evm_block_from_rpc_by_hash = client.get_block_by_hash(block_hash, true).await?.expect("Block should exist"); // Both blocks should be identical assert_eq!( evm_block_from_rpc_by_number, evm_block_from_rpc_by_hash, "Hydrated EVM blocks should match" ); // Verify transaction info let unsigned_tx = tx .generic_transaction() .try_into_unsigned() .expect("Transaction shall be converted"); let signed_tx = signer_copy.sign_transaction(unsigned_tx); let expected_tx_info = TransactionInfo::new(&receipt, signed_tx); let tx_info = if let HashesOrTransactionInfos::TransactionInfos(tx_infos) = evm_block_from_rpc_by_number.transactions { tx_infos[0].clone() } else { panic!("Expected hydrated transactions"); }; assert_eq!(expected_tx_info, tx_info, "TransationInfos should match"); Ok(()) } async fn test_block_hash_for_tag_with_proper_ethereum_block_hash_works( client: Arc, ) -> anyhow::Result<()> { // Deploy a transaction to create a block with transactions let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?; let value = U256::from(5_000_000_000_000u128); let tx = TransactionBuilder::new(&client) .value(value) .input(bytes.to_vec()) .send() .await?; let receipt = tx.wait_for_receipt().await?; let ethereum_block_hash = receipt.block_hash; log::trace!(target: LOG_TARGET, "Testing with Ethereum block hash: {ethereum_block_hash:?}"); let block_by_hash = client .get_block_by_hash(ethereum_block_hash, false) .await? .expect("Block should exist"); let account = Account::default(); let balance = client.get_balance(account.address(), ethereum_block_hash.into()).await?; assert!(balance >= U256::zero(), "Balance should be retrievable with Ethereum hash"); assert_eq!(block_by_hash.hash, ethereum_block_hash, "Block hash should match"); Ok(()) } async fn test_block_hash_for_tag_with_invalid_ethereum_block_hash_fails( client: Arc, ) -> anyhow::Result<()> { let fake_eth_hash = H256::from([0x42u8; 32]); log::trace!(target: LOG_TARGET, "Testing with fake Ethereum hash: {fake_eth_hash:?}"); let account = Account::default(); let result = client.get_balance(account.address(), fake_eth_hash.into()).await; assert!(result.is_err(), "Should fail with non-existent Ethereum hash"); Ok(()) } async fn test_block_hash_for_tag_with_block_number_works( client: Arc, ) -> anyhow::Result<()> { let block_number = client.block_number().await?; log::trace!(target: LOG_TARGET, "Testing with block number: {block_number}"); let account = Account::default(); let balance = client .get_balance(account.address(), BlockNumberOrTagOrHash::BlockNumber(block_number)) .await?; assert!(balance >= U256::zero(), "Balance should be retrievable with block number"); Ok(()) } async fn test_block_hash_for_tag_with_block_tags_works( client: Arc, ) -> anyhow::Result<()> { let account = Account::default(); let tags = vec![ BlockTag::Latest, BlockTag::Finalized, BlockTag::Safe, BlockTag::Earliest, BlockTag::Pending, ]; for tag in tags { let balance = client.get_balance(account.address(), tag.clone().into()).await?; assert!(balance >= U256::zero(), "Balance should be retrievable with tag {tag:?}"); } Ok(()) } async fn test_multiple_transactions_in_block(client: Arc) -> anyhow::Result<()> { let num_transactions = 20; let alith = Account::default(); let ethan = Account::from(subxt_signer::eth::dev::ethan()); let amount = U256::from(1_000_000_000_000_000_000u128); // Prepare EVM transfer transactions let transactions = prepare_evm_transactions(&client, alith, ethan.address(), amount, num_transactions).await?; // Submit all transactions let submitted_txs = submit_evm_transactions(transactions).await?; let tx_hashes: Vec = submitted_txs.iter().map(|(hash, _, _)| *hash).collect(); log::trace!(target: LOG_TARGET, "Submitted {} transactions", submitted_txs.len()); // All transactions should be included in the same block since nonces are in descending order let first_receipt = submitted_txs[0].2.wait_for_receipt().await?; // Fetch and verify block contains all transactions verify_transactions_in_single_block(&client, first_receipt.block_number, &tx_hashes).await?; Ok(()) } async fn test_mixed_evm_bizinikiwi_transactions(client: Arc) -> anyhow::Result<()> { let num_evm_txs = 10; let num_bizinikiwi_txs = 7; let alith = Account::default(); let ethan = Account::from(subxt_signer::eth::dev::ethan()); let amount = U256::from(500_000_000_000_000_000u128); // Prepare EVM transactions log::trace!(target: LOG_TARGET, "Creating {num_evm_txs} EVM transfer transactions"); let evm_transactions = prepare_evm_transactions(&client, alith, ethan.address(), amount, num_evm_txs).await?; // Prepare bizinikiwi transactions (simple remarks) log::trace!(target: LOG_TARGET, "Creating {num_bizinikiwi_txs} bizinikiwi remark transactions"); let alice_signer = subxt_signer::sr25519::dev::alice(); let (node_client, _, _) = client::connect(SharedResources::pez_node_rpc_url()).await.unwrap(); let bizinikiwi_txs = prepare_bizinikiwi_transactions(&node_client, &alice_signer, num_bizinikiwi_txs).await?; log::trace!(target: LOG_TARGET, "Submitting {num_evm_txs} EVM and {num_bizinikiwi_txs} bizinikiwi transactions"); // Submit EVM transactions let evm_submitted = submit_evm_transactions(evm_transactions).await?; let evm_tx_hashes: Vec = evm_submitted.iter().map(|(hash, _, _)| *hash).collect(); // Submit bizinikiwi transactions let bizinikiwi_futures = submit_bizinikiwi_transactions(bizinikiwi_txs).await; // Wait for first EVM receipt and all bizinikiwi transactions in parallel let (evm_first_receipt_result, _bizinikiwi_results) = tokio::join!( async { evm_submitted[0].2.wait_for_receipt().await }, futures::future::join_all(bizinikiwi_futures) ); // Handle the EVM receipt result let evm_first_receipt = evm_first_receipt_result?; // Fetch and verify block contains all transactions verify_transactions_in_single_block(&client, evm_first_receipt.block_number, &evm_tx_hashes) .await?; Ok(()) } async fn test_runtime_pallets_address_upload_code(client: Arc) -> anyhow::Result<()> { let (node_client, pez_node_rpc_client, _) = client::connect(SharedResources::pez_node_rpc_url()).await?; let (bytecode, _) = pezpallet_revive_fixtures::compile_module("dummy")?; let signer = Account::default(); // Helper function to get bizinikiwi block hash from EVM block number let get_bizinikiwi_block_hash = |block_number: U256| { let rpc_client = pez_node_rpc_client.clone(); async move { rpc_client .request::("chain_getBlockHash", rpc_params![block_number]) .await } }; // Step 1: Encode the Bizinikiwi upload_code call let upload_call = subxt::dynamic::tx( "Revive", "upload_code", vec![ subxt::dynamic::Value::from_bytes(&bytecode), subxt::dynamic::Value::u128(u128::max_value()), // storage_deposit_limit ], ); let encoded_call = node_client.tx().call_data(&upload_call)?; // Step 2: Send the encoded call to RUNTIME_PALLETS_ADDR let tx = TransactionBuilder::new(&client) .signer(signer.clone()) .to(pezpallet_revive::RUNTIME_PALLETS_ADDR) .input(encoded_call.clone()) .send() .await?; // Step 3: Wait for receipt let receipt = tx.wait_for_receipt().await?; // Step 4: Verify transaction was successful assert_eq!( receipt.status.unwrap_or(U256::zero()), U256::one(), "Transaction should be successful" ); // Step 5: Verify the code was actually uploaded let code_hash = H256(pezsp_io::hashing::keccak_256(&bytecode)); let query = subxt_client::storage().revive().pristine_code(code_hash); let block_hash: pezsp_core::H256 = get_bizinikiwi_block_hash(receipt.block_number).await?; let stored_code = node_client.storage().at(block_hash).fetch(&query).await?; assert!(stored_code.is_some(), "Code with hash {code_hash:?} should exist in storage"); assert_eq!(stored_code.unwrap(), bytecode, "Stored code should match the uploaded bytecode"); Ok(()) }