diff --git a/Cargo.lock b/Cargo.lock index cc5cbef..92c61d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3174,9 +3174,11 @@ dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", "jsonrpsee-http-client", + "jsonrpsee-server", "jsonrpsee-types", "jsonrpsee-wasm-client", "jsonrpsee-ws-client", + "tokio", ] [[package]] @@ -3218,7 +3220,9 @@ dependencies = [ "http-body", "http-body-util", "jsonrpsee-types", + "parking_lot", "pin-project", + "rand 0.8.6", "rustc-hash 2.1.3", "serde", "serde_json", @@ -3254,6 +3258,33 @@ dependencies = [ "url", ] +[[package]] +name = "jsonrpsee-server" +version = "0.24.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c625c78b8d545478370b6e7a2a191b0d921f831a9eef38dc1e7efb57e7a5472f" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tokio-util", + "tower 0.4.13", + "tracing", +] + [[package]] name = "jsonrpsee-types" version = "0.24.11" @@ -5354,6 +5385,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + [[package]] name = "rpassword" version = "7.5.4" @@ -6300,6 +6337,7 @@ dependencies = [ "base64", "bytes", "futures", + "http", "httparse", "log", "rand 0.8.6", @@ -7218,6 +7256,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5a7e16d..9ff358a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,6 +142,7 @@ qp-wormhole-circuit-builder = { version = "4.3.0" } sha2 = "0.10" [dev-dependencies] +jsonrpsee = { version = "0.24", features = ["server"] } qp-poseidon-core = "3.1.0" serial_test = "3.1" tempfile = "3.8.1" diff --git a/LIBRARY_USAGE.md b/LIBRARY_USAGE.md index 95a7b16..af55012 100644 --- a/LIBRARY_USAGE.md +++ b/LIBRARY_USAGE.md @@ -51,6 +51,16 @@ async fn main() -> Result<(), Box> { } ``` +`QuantusClient::new` reads the runtime version and metadata from the current best block, so a +runtime upgrade is visible as soon as it enacts. To decode an older block, ask for a client pinned +to that block. It shares the connection and only fetches metadata when the block ran a different +runtime: + +```rust +let at_finalized = client.at_block(finalized_hash).await?; +let events = at_finalized.client().blocks().at(finalized_hash).await?.events().await?; +``` + ### 3. Loading a Wallet for Transactions ```rust diff --git a/src/chain/client.rs b/src/chain/client.rs index a72874a..d671f31 100644 --- a/src/chain/client.rs +++ b/src/chain/client.rs @@ -4,14 +4,19 @@ //! across all CLI modules. use crate::{error::QuantusError, log_verbose}; -use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; +use jsonrpsee::{ + core::client::ClientT, + ws_client::{WsClient, WsClientBuilder}, +}; use qp_dilithium_crypto::types::DilithiumSignatureScheme; use sp_core::crypto::AccountId32; use sp_runtime::{traits::IdentifyAccount, MultiAddress}; use std::{sync::Arc, time::Duration}; use subxt::{ - backend::rpc::RpcClient, + backend::{legacy::LegacyBackend, rpc::RpcClient, Backend, BackendExt}, + client::RuntimeVersion, config::{substrate::SubstrateHeader, DefaultExtrinsicParams}, + utils::H256, Config, OnlineClient, }; use subxt_metadata::Metadata as SubxtMetadata; @@ -131,39 +136,74 @@ impl QuantusClient { QuantusError::NetworkError(error_msg) })?; - // Wrap WS client in Arc for sharing let ws_client = Arc::new(ws_client); + let backend = Self::backend(&ws_client); - // Create RPC client wrapper for subxt - let rpc_client = RpcClient::new(ws_client.clone()); - - // Create SubXT client using the configured RPC client - let client = OnlineClient::::from_rpc_client(rpc_client).await?; + // subxt's own constructor pins metadata to the latest finalized block. QPoW finality + // trails the head by ~100 blocks, so for ~20 minutes after an upgrade enacts that + // metadata describes the old runtime while the head already runs the new one. Read + // the runtime version and metadata from one best-block hash instead, so the pair + // cannot straddle an upgrade either. + let best = best_block_hash(&ws_client).await?; + let (version_json, runtime_version) = fetch_runtime_version(&ws_client, Some(best)).await?; // Reject non-Quantus / older-unsupported runtimes before encode/sign. Newer-than-table // Quantus specs are allowed with a warning (see validate_runtime_identity). if enforce_runtime_identity { - use jsonrpsee::core::client::ClientT; - let runtime_version: serde_json::Value = ws_client - .request::("state_getRuntimeVersion", []) - .await - .map_err(|e| { - QuantusError::NetworkError(format!("Failed to fetch runtime version: {e:?}")) - })?; - crate::config::validate_runtime_version_value(&runtime_version).map_err( - |e| match e { - QuantusError::NetworkError(msg) => - QuantusError::NetworkError(format!("{msg} (from {display_node_url})")), - other => other, - }, - )?; + crate::config::validate_runtime_version_value(&version_json).map_err(|e| match e { + QuantusError::NetworkError(msg) => + QuantusError::NetworkError(format!("{msg} (from {display_node_url})")), + other => other, + })?; } + log_verbose!( + "๐Ÿ“ก Head {:?} runs spec {} / tx {}", + best, + runtime_version.spec_version, + runtime_version.transaction_version + ); + let genesis_hash = backend.genesis_hash().await?; + let metadata = fetch_metadata_at(&backend, best).await?; + let client = + OnlineClient::from_backend_with(genesis_hash, runtime_version, metadata, backend)?; + log_verbose!("โœ… Connected to Quantus node successfully!"); Ok(QuantusClient { client, rpc_client: ws_client, node_url: node_url.to_string() }) } + fn backend(ws_client: &Arc) -> Arc> { + Arc::new(LegacyBackend::builder().build(RpcClient::new(ws_client.clone()))) + } + + /// A client whose metadata and runtime version are the ones `hash` was produced under. + /// + /// Use it to decode that block's events, extrinsics and storage. Reads at the head keep + /// using `self`, which is returned unchanged when `hash` runs the same runtime. Never + /// sign with the result: the chain verifies signatures against the head runtime. + pub async fn at_block(&self, hash: H256) -> crate::error::Result { + let (_, runtime_version) = fetch_runtime_version(&self.rpc_client, Some(hash)).await?; + if runtime_version == self.client.runtime_version() { + return Ok(self.clone()); + } + log_verbose!( + "๐Ÿ“ก Block {:?} runs spec {} / tx {}; decoding it with that runtime's metadata", + hash, + runtime_version.spec_version, + runtime_version.transaction_version + ); + let backend = Self::backend(&self.rpc_client); + let metadata = fetch_metadata_at(&backend, hash).await?; + let client = OnlineClient::from_backend_with( + self.client.genesis_hash(), + runtime_version, + metadata, + backend, + )?; + Ok(Self { client, rpc_client: self.rpc_client.clone(), node_url: self.node_url.clone() }) + } + /// Get reference to the underlying SubXT client /// The FIPS 204 context the connected runtime verifies extrinsic signatures under. Read from /// the runtime version subxt already cached at connect, so this costs no RPC. @@ -193,19 +233,7 @@ impl QuantusClient { /// This bypasses SubXT's default behavior of using finalized blocks pub async fn get_latest_block(&self) -> crate::error::Result { log_verbose!("๐Ÿ” Fetching latest block hash via RPC..."); - - // Use RPC call to get the latest block hash - use jsonrpsee::core::client::ClientT; - let latest_hash: subxt::utils::H256 = self - .rpc_client - .request::("chain_getBlockHash", []) - .await - .map_err(|e| { - crate::error::QuantusError::NetworkError(format!( - "Failed to fetch latest block hash: {e:?}" - )) - })?; - + let latest_hash = best_block_hash(&self.rpc_client).await?; log_verbose!("๐Ÿ“ฆ Latest block hash: {:?}", latest_hash); Ok(latest_hash) } @@ -259,7 +287,6 @@ impl QuantusClient { pub async fn get_genesis_hash(&self) -> crate::error::Result { log_verbose!("๐Ÿ” Fetching genesis hash via RPC..."); - use jsonrpsee::core::client::ClientT; let genesis_hash: subxt::utils::H256 = self .rpc_client .request::("chain_getBlockHash", [0u32]) @@ -277,39 +304,19 @@ impl QuantusClient { /// Get runtime version using RPC call pub async fn get_runtime_version(&self) -> crate::error::Result<(u32, u32)> { log_verbose!("๐Ÿ” Fetching runtime version via RPC..."); - - use jsonrpsee::core::client::ClientT; - let runtime_version: serde_json::Value = self - .rpc_client - .request::("state_getRuntimeVersion", []) - .await - .map_err(|e| { - crate::error::QuantusError::NetworkError(format!( - "Failed to fetch runtime version: {e:?}" - )) - })?; - - let spec_version = runtime_version["specVersion"].as_u64().ok_or_else(|| { - crate::error::QuantusError::NetworkError("Failed to parse spec version".to_string()) - })? as u32; - - let transaction_version = - runtime_version["transactionVersion"].as_u64().ok_or_else(|| { - crate::error::QuantusError::NetworkError( - "Failed to parse transaction version".to_string(), - ) - })? as u32; - - log_verbose!("๐Ÿ”ง Runtime version: spec={}, tx={}", spec_version, transaction_version); - Ok((spec_version, transaction_version)) + let (_, version) = fetch_runtime_version(&self.rpc_client, None).await?; + log_verbose!( + "๐Ÿ”ง Runtime version: spec={}, tx={}", + version.spec_version, + version.transaction_version + ); + Ok((version.spec_version, version.transaction_version)) } /// Get runtime hash using RPC call (if available) pub async fn get_runtime_hash(&self) -> crate::error::Result> { log_verbose!("๐Ÿ” Fetching runtime hash via RPC..."); - use jsonrpsee::core::client::ClientT; - // Try different possible RPC calls for runtime hash let possible_calls = ["state_getRuntimeHash", "state_getRuntime", "chain_getRuntimeHash"]; @@ -336,6 +343,55 @@ impl QuantusClient { } } +async fn best_block_hash(ws_client: &WsClient) -> crate::error::Result { + ws_client.request::("chain_getBlockHash", []).await.map_err(|e| { + QuantusError::NetworkError(format!("Failed to fetch latest block hash: {e:?}")) + }) +} + +/// `state_getRuntimeVersion` at `at`, or at the head for `None`: the raw JSON and the parsed pair. +async fn fetch_runtime_version( + ws_client: &WsClient, + at: Option, +) -> crate::error::Result<(serde_json::Value, RuntimeVersion)> { + let value: serde_json::Value = + ws_client.request("state_getRuntimeVersion", [at]).await.map_err(|e| { + QuantusError::NetworkError(format!("Failed to fetch runtime version: {e:?}")) + })?; + let version = parse_runtime_version(&value)?; + Ok((value, version)) +} + +fn parse_runtime_version(value: &serde_json::Value) -> crate::error::Result { + let field = |name: &str| { + value + .get(name) + .and_then(serde_json::Value::as_u64) + .and_then(|v| u32::try_from(v).ok()) + .ok_or_else(|| { + QuantusError::NetworkError(format!("Runtime version has no usable `{name}`")) + }) + }; + Ok(RuntimeVersion { + spec_version: field("specVersion")?, + transaction_version: field("transactionVersion")?, + }) +} + +/// The newest metadata version the runtime at `at` serves, negotiated the way subxt does. +async fn fetch_metadata_at( + backend: &LegacyBackend, + at: H256, +) -> crate::error::Result { + for version in subxt_metadata::SUPPORTED_METADATA_VERSIONS { + match backend.metadata_at_version(version, at).await { + Ok(metadata) => return Ok(metadata), + Err(e) => log_verbose!("Metadata v{} unavailable at {:?}: {}", version, at, e), + } + } + Ok(backend.legacy_metadata(at).await?) +} + /// Scheme-aware subxt signer (ML-DSA-65 or ML-DSA-87). /// /// Pairs are boxed: Dilithium secret material is multiโ€‘KB, and an unboxed enum @@ -389,6 +445,118 @@ impl subxt::tx::Signer for QuantusSigner { #[cfg(test)] mod tests { use super::*; + use codec::Encode; + use jsonrpsee::{ + server::{RpcModule, Server, ServerHandle}, + types::ErrorObjectOwned, + }; + use serde_json::json; + use std::sync::Mutex; + + const OLD_RUNTIME: RuntimeVersion = + RuntimeVersion { spec_version: 144, transaction_version: 3 }; + const NEW_RUNTIME: RuntimeVersion = + RuntimeVersion { spec_version: 148, transaction_version: 6 }; + const GENESIS: H256 = H256([0x01; 32]); + const FINALIZED: H256 = H256([0x44; 32]); + const HEAD: H256 = H256([0x48; 32]); + + fn runtime_at(hash: H256) -> RuntimeVersion { + if hash == HEAD { + NEW_RUNTIME + } else { + OLD_RUNTIME + } + } + + /// A node caught mid-upgrade the way Heisenberg is for ~20 minutes after every enactment: + /// the head runs the new runtime, the finalized block still runs the old one. Records the + /// block named by every metadata request. + async fn mock_node() -> (String, Arc>>, ServerHandle) { + let metadata_requests = Arc::new(Mutex::new(Vec::new())); + let server = Server::builder().build("127.0.0.1:0").await.expect("bind mock node"); + let url = format!("ws://{}", server.local_addr().expect("mock node address")); + let mut module = RpcModule::new(metadata_requests.clone()); + module + .register_method("chain_getBlockHash", |params, _, _| { + let number: Option = params.sequence().optional_next()?; + Ok::<_, ErrorObjectOwned>(if number == Some(0) { GENESIS } else { HEAD }) + }) + .expect("register"); + module + .register_method("chain_getFinalizedHead", |_, _, _| { + Ok::<_, ErrorObjectOwned>(FINALIZED) + }) + .expect("register"); + module + .register_method("state_getRuntimeVersion", |params, _, _| { + let at: Option = params.sequence().optional_next()?; + let version = runtime_at(at.unwrap_or(HEAD)); + Ok::<_, ErrorObjectOwned>(json!({ + "specName": crate::config::EXPECTED_RUNTIME_SPEC_NAME, + "specVersion": version.spec_version, + "transactionVersion": version.transaction_version, + })) + }) + .expect("register"); + module + .register_method("state_call", |params, requests: &Arc>>, _| { + let mut params = params.sequence(); + let function: String = params.next()?; + let _encoded_args: String = params.next()?; + let at: Option = params.optional_next()?; + assert_eq!(function, "Metadata_metadata_at_version"); + requests + .lock() + .expect("lock") + .push(at.expect("metadata request must name a block")); + let metadata: &[u8] = include_bytes!("../quantus_metadata.scale"); + Ok::<_, ErrorObjectOwned>(format!( + "0x{}", + hex::encode(Some(metadata.to_vec()).encode()) + )) + }) + .expect("register"); + (url, metadata_requests, server.start(module)) + } + + #[tokio::test] + async fn connect_reads_runtime_version_and_metadata_from_one_head_block() { + let (url, metadata_requests, _node) = mock_node().await; + let client = QuantusClient::new(&url).await.expect("connect"); + assert_eq!(client.client().runtime_version(), NEW_RUNTIME); + assert_eq!(client.client().genesis_hash(), GENESIS); + assert_eq!(*metadata_requests.lock().expect("lock"), vec![HEAD]); + } + + #[tokio::test] + async fn at_block_decodes_a_pre_upgrade_block_with_the_runtime_that_produced_it() { + let (url, metadata_requests, _node) = mock_node().await; + let head = QuantusClient::new(&url).await.expect("connect"); + + let old = head.at_block(FINALIZED).await.expect("client at the finalized block"); + assert_eq!(old.client().runtime_version(), OLD_RUNTIME); + assert_eq!(head.client().runtime_version(), NEW_RUNTIME, "head client must be untouched"); + assert_eq!(*metadata_requests.lock().expect("lock"), vec![HEAD, FINALIZED]); + + let same = head.at_block(HEAD).await.expect("client at the head block"); + assert_eq!(same.client().runtime_version(), NEW_RUNTIME); + assert_eq!( + *metadata_requests.lock().expect("lock"), + vec![HEAD, FINALIZED], + "same runtime: metadata is not fetched again" + ); + } + + #[test] + fn parse_runtime_version_rejects_missing_fields() { + assert!(parse_runtime_version(&json!({ "specVersion": 148 })).is_err()); + assert_eq!( + parse_runtime_version(&json!({ "specVersion": 148, "transactionVersion": 6 })) + .expect("parse"), + NEW_RUNTIME + ); + } #[tokio::test] async fn quantus_client_new_redacts_userinfo_in_invalid_url_error() { diff --git a/src/cli/block.rs b/src/cli/block.rs index 5fdc6bd..1a7084b 100644 --- a/src/cli/block.rs +++ b/src/cli/block.rs @@ -113,33 +113,31 @@ async fn handle_block_analyze_command( let quantus_client = QuantusClient::new(node_url).await?; - // Determine which block to analyze - let (block_number, block_hash) = if let Some(num) = number { - // Convert number to hash using our storage function - let hash = storage::resolve_block_hash(&quantus_client, &num.to_string()).await?; - (num, hash) + let block_hash = if let Some(num) = number { + storage::resolve_block_hash(&quantus_client, &num.to_string()).await? } else if let Some(h) = hash { - // Parse hash and get block number from storage - let parsed_hash = storage::resolve_block_hash(&quantus_client, &h).await?; - // Get block number by querying System::Number at that block - let storage_at = quantus_client.client().storage().at(parsed_hash); - let number_addr = crate::chain::quantus_subxt::api::storage().system().number(); - let block_num = storage_at.fetch_or_default(&number_addr).await.map_err(|e| { - QuantusError::NetworkError(format!("Failed to get block number: {e:?}")) - })?; - (block_num, parsed_hash) + storage::resolve_block_hash(&quantus_client, &h).await? } else if latest { - // Use latest block - let hash = quantus_client.get_latest_block().await?; - let storage_at = quantus_client.client().storage().at(hash); - let number_addr = crate::chain::quantus_subxt::api::storage().system().number(); - let block_num = storage_at.fetch_or_default(&number_addr).await.map_err(|e| { - QuantusError::NetworkError(format!("Failed to get latest block number: {e:?}")) - })?; - (block_num, hash) + quantus_client.get_latest_block().await? } else { return Err(QuantusError::Generic("Must specify --number, --hash, or --latest".to_string())); }; + let quantus_client = quantus_client.at_block(block_hash).await?; + let block_number = match number { + Some(num) => num, + None => { + let number_addr = crate::chain::quantus_subxt::api::storage().system().number(); + quantus_client + .client() + .storage() + .at(block_hash) + .fetch_or_default(&number_addr) + .await + .map_err(|e| { + QuantusError::NetworkError(format!("Failed to get block number: {e:?}")) + })? + }, + }; log_print!("๐Ÿ“ฆ Block #{} - {:#x}", block_number, block_hash); log_print!(""); diff --git a/src/cli/events.rs b/src/cli/events.rs index 1eb8fac..d202510 100644 --- a/src/cli/events.rs +++ b/src/cli/events.rs @@ -119,7 +119,7 @@ pub async fn handle_events_command( log_print!("๐Ÿ”ฎ Quantus CLI"); log_print!("๐ŸŽฏ Found Block #{}", block_number); - // Get events from the block + let quantus_client = quantus_client.at_block(block_hash).await?; let events = quantus_client.client().blocks().at(block_hash).await?.events().await?; log_print!("๐Ÿ“‹ Block Events:"); diff --git a/src/cli/exercise/scenarios/upgrade.rs b/src/cli/exercise/scenarios/upgrade.rs index eb70012..c1c381e 100644 --- a/src/cli/exercise/scenarios/upgrade.rs +++ b/src/cli/exercise/scenarios/upgrade.rs @@ -367,7 +367,8 @@ async fn find_code_updated_since( } for block_hash in chain.into_iter().rev() { - let events = ctx.client.client().blocks().at(block_hash).await?.events().await?; + let at_block = ctx.client.at_block(block_hash).await?; + let events = at_block.client().blocks().at(block_hash).await?.events().await?; if events .find_first::() .map_err(|e| QuantusError::Generic(format!("failed to decode events: {e:?}")))? diff --git a/src/cli/storage.rs b/src/cli/storage.rs index 6cc0483..2f787a8 100644 --- a/src/cli/storage.rs +++ b/src/cli/storage.rs @@ -504,18 +504,18 @@ pub async fn iterate_storage_entries( limit.to_string().bright_yellow() ); - // Validate pallet exists - validate_pallet_exists(quantus_client.client(), pallet_name)?; - - // Determine block hash to use let block_hash = if let Some(block_id) = block_identifier { resolve_block_hash(quantus_client, &block_id).await? } else { quantus_client.get_latest_block().await? }; + let at_block = quantus_client.at_block(block_hash).await?; + let quantus_client = &at_block; log_verbose!("๐Ÿ“ฆ Using block: {:?}", block_hash); + validate_pallet_exists(quantus_client.client(), pallet_name)?; + // Try to get storage metadata to show what type of storage this is let metadata = quantus_client.client().metadata(); let pallet = metadata.pallet_by_name(pallet_name).unwrap(); @@ -714,13 +714,15 @@ async fn get_storage_by_parts( log_print!("๐Ÿ”‘ With key: {}", key_value.bright_yellow()); } - validate_pallet_exists(quantus_client.client(), &pallet)?; - let block_hash = if let Some(block_id) = &block { resolve_block_hash(quantus_client, block_id).await? } else { quantus_client.get_latest_block().await? }; + let at_block = quantus_client.at_block(block_hash).await?; + let quantus_client = &at_block; + + validate_pallet_exists(quantus_client.client(), &pallet)?; let entry_count = count_storage_entries(quantus_client, &pallet, &name, block_hash).await?; let is_storage_value = entry_count == 1; diff --git a/src/cli/wormhole.rs b/src/cli/wormhole.rs index 49d9149..5f3f083 100644 --- a/src/cli/wormhole.rs +++ b/src/cli/wormhole.rs @@ -1282,7 +1282,8 @@ fn show_wormhole_address(secret_file: String) -> crate::error::Result<()> { Ok(()) } -/// Fetch the latest finalized block as a fully materialised subxt `Block`. +/// Fetch the latest finalized block as a fully materialised subxt `Block`, decoded with the +/// runtime that produced it (still the pre-upgrade one while the head has moved on). /// /// Uses [`crate::error::Result`] (not `anyhow`) so it composes with the rest /// of the SDK surface. Network/decoding failures are wrapped in @@ -1299,7 +1300,8 @@ pub async fn at_finalized_block( "Failed to fetch finalized block hash: {e:?}" )) })?; - let block = quantus_client.client().blocks().at(finalized_block).await.map_err(|e| { + let at_finalized = quantus_client.at_block(finalized_block).await?; + let block = at_finalized.client().blocks().at(finalized_block).await.map_err(|e| { crate::error::QuantusError::NetworkError(format!( "Failed to fetch finalized block {finalized_block:?}: {e:?}" )) @@ -2324,22 +2326,17 @@ async fn execute_initial_transfers( // Query transfer counts BEFORE submitting the batch. // The transfer_count used in the proof is the count at the time of transfer, // which equals the count before the transfer (since it increments after). - let client = quantus_client.client(); - let tip_block_hash = wormhole_tip_block(quantus_client, execution_mode) - .await - .map_err(|e| { - crate::error::QuantusError::Generic(format!( - "Failed to get tip block for transfer counts: {}", - e - )) - })? - .hash(); + let tip_block = wormhole_tip_block(quantus_client, execution_mode).await.map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to get tip block for transfer counts: {}", + e + )) + })?; let mut transfer_counts_before: Vec = Vec::with_capacity(num_proofs); for secret in secrets.iter() { let wormhole_address = SubxtAccountId(*secret.address()); - let count = client + let count = tip_block .storage() - .at(tip_block_hash) .fetch(&quantus_node::api::storage().wormhole().transfer_count(wormhole_address)) .await .map_err(|e| { @@ -3777,19 +3774,14 @@ async fn run_dissolve( let initial_secret = derive_wormhole_secret(&wallet.mnemonic, 0, 1)?; let wormhole_address = SubxtAccountId(*initial_secret.address()); - let tip_block_hash = wormhole_tip_block(&quantus_client, execution_mode) - .await - .map_err(|e| { - crate::error::QuantusError::Generic(format!( - "Failed to get tip block for dissolve transfer count: {}", - e - )) - })? - .hash(); - let transfer_count_before = quantus_client - .client() + let tip_block = wormhole_tip_block(&quantus_client, execution_mode).await.map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to get tip block for dissolve transfer count: {}", + e + )) + })?; + let transfer_count_before = tip_block .storage() - .at(tip_block_hash) .fetch(&quantus_node::api::storage().wormhole().transfer_count(wormhole_address.clone())) .await .map_err(|e| {