From 4c1c0434f5e526d8f41f5c950f0e8ed7fc8579bc Mon Sep 17 00:00:00 2001 From: illuzen Date: Sun, 25 Jan 2026 12:28:25 +0800 Subject: [PATCH 1/7] prefix-filtering --- Cargo.lock | 218 +++++++++++++++++++++++++++- Cargo.toml | 5 + src/chain/quantus_subxt.rs | 4 +- src/cli/batch.rs | 10 +- src/cli/block.rs | 14 +- src/cli/common.rs | 15 +- src/cli/generic_call.rs | 45 +++--- src/cli/mod.rs | 88 +++++++---- src/cli/referenda.rs | 49 ++++--- src/cli/referenda_decode.rs | 5 +- src/cli/reversible.rs | 5 +- src/cli/scheduler.rs | 5 +- src/cli/storage.rs | 25 ++-- src/cli/tech_collective.rs | 5 +- src/cli/tech_referenda.rs | 45 +++--- src/cli/transfers.rs | 281 ++++++++++++++++++++++++++++++++++++ src/cli/treasury.rs | 20 ++- src/cli/wallet.rs | 10 +- src/lib.rs | 1 + src/main.rs | 1 + src/subsquid/client.rs | 231 +++++++++++++++++++++++++++++ src/subsquid/hash.rs | 175 ++++++++++++++++++++++ src/subsquid/mod.rs | 13 ++ src/subsquid/types.rs | 263 +++++++++++++++++++++++++++++++++ 24 files changed, 1396 insertions(+), 137 deletions(-) create mode 100644 src/cli/transfers.rs create mode 100644 src/subsquid/client.rs create mode 100644 src/subsquid/hash.rs create mode 100644 src/subsquid/mod.rs create mode 100644 src/subsquid/types.rs diff --git a/Cargo.lock b/Cargo.lock index 6191415..734f1b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -784,6 +784,20 @@ dependencies = [ "constant_time_eq 0.3.1", ] +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", + "constant_time_eq 0.4.2", + "cpufeatures", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -891,6 +905,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -1077,6 +1097,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.7.1" @@ -1930,8 +1956,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1941,9 +1969,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.5+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -2238,6 +2268,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -2246,6 +2277,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64", "bytes", "futures-channel", "futures-core", @@ -2253,7 +2285,9 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -2509,6 +2543,22 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2673,7 +2723,7 @@ dependencies = [ "serde_json", "thiserror 1.0.69", "tokio", - "tower", + "tower 0.4.13", "tracing", "url", ] @@ -2861,6 +2911,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "macro_magic" version = "0.5.1" @@ -3738,6 +3794,8 @@ version = "0.3.1" dependencies = [ "aes-gcm", "argon2", + "base64", + "blake3", "chrono", "clap", "colored", @@ -3752,6 +3810,7 @@ dependencies = [ "qp-rusty-crystals-dilithium", "qp-rusty-crystals-hdwallet", "rand 0.9.2", + "reqwest", "rpassword", "serde", "serde_json", @@ -3766,6 +3825,61 @@ dependencies = [ "toml 0.9.5", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.16", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.16", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.40" @@ -3915,6 +4029,44 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -4033,6 +4185,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] @@ -4417,6 +4570,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serdect" version = "0.2.0" @@ -5404,6 +5569,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -5717,6 +5891,39 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -6233,6 +6440,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index b4ec172..c7473f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,11 @@ qp-rusty-crystals-hdwallet = { version = "1.0.0" } qp-poseidon = { version = "1.0.1", default-features = false } qp-dilithium-crypto = { version = "0.2.0", features = ["serde"] } +# HTTP client for Subsquid queries +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } +blake3 = "1.8" +base64 = "0.22" + # Blockchain and RPC client codec = { package = "parity-scale-codec", version = "3.7", features = [ "derive", diff --git a/src/chain/quantus_subxt.rs b/src/chain/quantus_subxt.rs index a94a0d2..d2c9f6d 100644 --- a/src/chain/quantus_subxt.rs +++ b/src/chain/quantus_subxt.rs @@ -1995,8 +1995,8 @@ pub mod api { .only_these_pallets(&PALLETS) .only_these_runtime_apis(&RUNTIME_APIS) .hash(); - runtime_metadata_hash == - [ + runtime_metadata_hash + == [ 194u8, 46u8, 30u8, 103u8, 67u8, 25u8, 224u8, 42u8, 104u8, 224u8, 105u8, 213u8, 149u8, 58u8, 199u8, 151u8, 221u8, 215u8, 141u8, 247u8, 109u8, 85u8, 204u8, 202u8, 96u8, 104u8, 173u8, 94u8, 198u8, 124u8, 113u8, 174u8, diff --git a/src/cli/batch.rs b/src/cli/batch.rs index b1b84c8..0449979 100644 --- a/src/cli/batch.rs +++ b/src/cli/batch.rs @@ -75,7 +75,7 @@ pub async fn handle_batch_command( count, to, amount, - } => + } => { handle_batch_send_command( from, node_url, @@ -88,9 +88,11 @@ pub async fn handle_batch_command( amount, execution_mode, ) - .await, - BatchCommands::Config { limits, info } => - handle_batch_config_command(node_url, limits, info).await, + .await + }, + BatchCommands::Config { limits, info } => { + handle_batch_config_command(node_url, limits, info).await + }, } } diff --git a/src/cli/block.rs b/src/cli/block.rs index a5c6b1c..b085592 100644 --- a/src/cli/block.rs +++ b/src/cli/block.rs @@ -79,7 +79,7 @@ pub async fn handle_block_command( extrinsics_details, events, all, - } => + } => { handle_block_analyze_command( number, hash, @@ -91,9 +91,11 @@ pub async fn handle_block_command( all, node_url, ) - .await, - BlockCommands::List { start, end, step } => - handle_block_list_command(start, end, step, node_url).await, + .await + }, + BlockCommands::List { start, end, step } => { + handle_block_list_command(start, end, step, node_url).await + }, } } @@ -138,7 +140,9 @@ async fn handle_block_analyze_command( })?; (block_num, hash) } else { - return Err(QuantusError::Generic("Must specify --number, --hash, or --latest".to_string())); + return Err(QuantusError::Generic( + "Must specify --number, --hash, or --latest".to_string(), + )); }; log_print!("📦 Block #{} - {:#x}", block_number, block_hash); diff --git a/src/cli/common.rs b/src/cli/common.rs index 32cda72..69a69d3 100644 --- a/src/cli/common.rs +++ b/src/cli/common.rs @@ -249,11 +249,11 @@ where let error_msg = format!("{e:?}"); // Check if it's a retryable error - let is_retryable = error_msg.contains("Priority is too low") || - error_msg.contains("Transaction is outdated") || - error_msg.contains("Transaction is temporarily banned") || - error_msg.contains("Transaction has a bad signature") || - error_msg.contains("Invalid Transaction"); + let is_retryable = error_msg.contains("Priority is too low") + || error_msg.contains("Transaction is outdated") + || error_msg.contains("Transaction is temporarily banned") + || error_msg.contains("Transaction has a bad signature") + || error_msg.contains("Invalid Transaction"); if is_retryable && attempt < 5 { log_verbose!( @@ -417,10 +417,11 @@ async fn wait_tx_inclusion( crate::log_verbose!(" Transaction status: {:?} (elapsed: {}s)", status, elapsed_secs); match status { - TxStatus::Validated => + TxStatus::Validated => { if let Some(ref pb) = spinner { pb.set_message(format!("Transaction validated ✓ ({}s)", elapsed_secs)); - }, + } + }, TxStatus::InBestBlock(tx_in_block) => { let block_hash = tx_in_block.block_hash(); crate::log_verbose!(" Transaction included in block: {:?}", block_hash); diff --git a/src/cli/generic_call.rs b/src/cli/generic_call.rs index fe2049a..a33f1e6 100644 --- a/src/cli/generic_call.rs +++ b/src/cli/generic_call.rs @@ -53,7 +53,7 @@ pub async fn execute_generic_call( let tx_hash = match (pallet, call) { // Balances pallet calls - ("Balances", "transfer_allow_death") => + ("Balances", "transfer_allow_death") => { submit_balance_transfer( quantus_client, from_keypair, @@ -62,8 +62,9 @@ pub async fn execute_generic_call( tip_amount, execution_mode, ) - .await?, - ("Balances", "transfer_keep_alive") => + .await? + }, + ("Balances", "transfer_keep_alive") => { submit_balance_transfer( quantus_client, from_keypair, @@ -72,40 +73,48 @@ pub async fn execute_generic_call( tip_amount, execution_mode, ) - .await?, + .await? + }, // System pallet calls - ("System", "remark") => + ("System", "remark") => { submit_system_remark(quantus_client, from_keypair, &args, tip_amount, execution_mode) - .await?, + .await? + }, // Sudo pallet calls ("Sudo", "sudo") => submit_sudo_call(quantus_client, from_keypair, &args).await?, // TechCollective pallet calls - ("TechCollective", "add_member") => + ("TechCollective", "add_member") => { submit_tech_collective_add_member(quantus_client, from_keypair, &args, execution_mode) - .await?, - ("TechCollective", "remove_member") => + .await? + }, + ("TechCollective", "remove_member") => { submit_tech_collective_remove_member( quantus_client, from_keypair, &args, execution_mode, ) - .await?, - ("TechCollective", "vote") => - submit_tech_collective_vote(quantus_client, from_keypair, &args, execution_mode).await?, + .await? + }, + ("TechCollective", "vote") => { + submit_tech_collective_vote(quantus_client, from_keypair, &args, execution_mode).await? + }, // ReversibleTransfers pallet calls - ("ReversibleTransfers", "schedule_transfer") => - submit_reversible_transfer(quantus_client, from_keypair, &args, execution_mode).await?, + ("ReversibleTransfers", "schedule_transfer") => { + submit_reversible_transfer(quantus_client, from_keypair, &args, execution_mode).await? + }, // Scheduler pallet calls - ("Scheduler", "schedule") => - submit_scheduler_schedule(quantus_client, from_keypair, &args).await?, - ("Scheduler", "cancel") => - submit_scheduler_cancel(quantus_client, from_keypair, &args).await?, + ("Scheduler", "schedule") => { + submit_scheduler_schedule(quantus_client, from_keypair, &args).await? + }, + ("Scheduler", "cancel") => { + submit_scheduler_cancel(quantus_client, from_keypair, &args).await? + }, // Generic fallback for unknown calls (_, _) => { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a5c768e..fcac116 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -22,6 +22,7 @@ pub mod storage; pub mod system; pub mod tech_collective; pub mod tech_referenda; +pub mod transfers; pub mod treasury; pub mod wallet; @@ -105,6 +106,10 @@ pub enum Commands { #[command(subcommand)] Treasury(treasury::TreasuryCommands), + /// Privacy-preserving transfer queries via Subsquid indexer + #[command(subcommand)] + Transfers(transfers::TransfersCommands), + /// Runtime management commands (requires root/sudo permissions) #[command(subcommand)] Runtime(runtime::RuntimeCommands), @@ -248,7 +253,7 @@ pub async fn execute_command( ) -> crate::error::Result<()> { match command { Commands::Wallet(wallet_cmd) => wallet::handle_wallet_command(wallet_cmd, node_url).await, - Commands::Send { from, to, amount, password, password_file, tip, nonce } => + Commands::Send { from, to, amount, password, password_file, tip, nonce } => { send::handle_send_command( from, to, @@ -260,41 +265,57 @@ pub async fn execute_command( nonce, execution_mode, ) - .await, - Commands::Batch(batch_cmd) => - batch::handle_batch_command(batch_cmd, node_url, execution_mode).await, - Commands::Reversible(reversible_cmd) => - reversible::handle_reversible_command(reversible_cmd, node_url, execution_mode).await, - Commands::HighSecurity(hs_cmd) => - high_security::handle_high_security_command(hs_cmd, node_url, execution_mode).await, - Commands::Recovery(recovery_cmd) => - recovery::handle_recovery_command(recovery_cmd, node_url, execution_mode).await, - Commands::Scheduler(scheduler_cmd) => - scheduler::handle_scheduler_command(scheduler_cmd, node_url, execution_mode).await, - Commands::Storage(storage_cmd) => - storage::handle_storage_command(storage_cmd, node_url, execution_mode).await, - Commands::TechCollective(tech_collective_cmd) => + .await + }, + Commands::Batch(batch_cmd) => { + batch::handle_batch_command(batch_cmd, node_url, execution_mode).await + }, + Commands::Reversible(reversible_cmd) => { + reversible::handle_reversible_command(reversible_cmd, node_url, execution_mode).await + }, + Commands::HighSecurity(hs_cmd) => { + high_security::handle_high_security_command(hs_cmd, node_url, execution_mode).await + }, + Commands::Recovery(recovery_cmd) => { + recovery::handle_recovery_command(recovery_cmd, node_url, execution_mode).await + }, + Commands::Scheduler(scheduler_cmd) => { + scheduler::handle_scheduler_command(scheduler_cmd, node_url, execution_mode).await + }, + Commands::Storage(storage_cmd) => { + storage::handle_storage_command(storage_cmd, node_url, execution_mode).await + }, + Commands::TechCollective(tech_collective_cmd) => { tech_collective::handle_tech_collective_command( tech_collective_cmd, node_url, execution_mode, ) - .await, - Commands::Preimage(preimage_cmd) => - preimage::handle_preimage_command(preimage_cmd, node_url, execution_mode).await, - Commands::TechReferenda(tech_referenda_cmd) => + .await + }, + Commands::Preimage(preimage_cmd) => { + preimage::handle_preimage_command(preimage_cmd, node_url, execution_mode).await + }, + Commands::TechReferenda(tech_referenda_cmd) => { tech_referenda::handle_tech_referenda_command( tech_referenda_cmd, node_url, execution_mode, ) - .await, - Commands::Referenda(referenda_cmd) => - referenda::handle_referenda_command(referenda_cmd, node_url, execution_mode).await, - Commands::Treasury(treasury_cmd) => - treasury::handle_treasury_command(treasury_cmd, node_url, execution_mode).await, - Commands::Runtime(runtime_cmd) => - runtime::handle_runtime_command(runtime_cmd, node_url, execution_mode).await, + .await + }, + Commands::Referenda(referenda_cmd) => { + referenda::handle_referenda_command(referenda_cmd, node_url, execution_mode).await + }, + Commands::Treasury(treasury_cmd) => { + treasury::handle_treasury_command(treasury_cmd, node_url, execution_mode).await + }, + Commands::Transfers(transfers_cmd) => { + transfers::handle_transfers_command(transfers_cmd).await + }, + Commands::Runtime(runtime_cmd) => { + runtime::handle_runtime_command(runtime_cmd, node_url, execution_mode).await + }, Commands::Call { pallet, call, @@ -305,7 +326,7 @@ pub async fn execute_command( tip, offline, call_data_only, - } => + } => { handle_generic_call_command( pallet, call, @@ -319,7 +340,8 @@ pub async fn execute_command( node_url, execution_mode, ) - .await, + .await + }, Commands::Balance { address } => { let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; @@ -339,11 +361,12 @@ pub async fn execute_command( Ok(()) }, }, - Commands::Events { block, block_hash, latest: _, finalized, pallet, raw, no_decode } => + Commands::Events { block, block_hash, latest: _, finalized, pallet, raw, no_decode } => { events::handle_events_command( block, block_hash, finalized, pallet, raw, !no_decode, node_url, ) - .await, + .await + }, Commands::System { runtime, metadata, rpc_methods } => { if runtime || metadata || rpc_methods { system::handle_system_extended_command( @@ -358,8 +381,9 @@ pub async fn execute_command( system::handle_system_command(node_url).await } }, - Commands::Metadata { no_docs, stats_only, pallet } => - metadata::handle_metadata_command(node_url, no_docs, stats_only, pallet).await, + Commands::Metadata { no_docs, stats_only, pallet } => { + metadata::handle_metadata_command(node_url, no_docs, stats_only, pallet).await + }, Commands::Version => { log_print!("CLI Version: Quantus CLI v{}", env!("CARGO_PKG_VERSION")); Ok(()) diff --git a/src/cli/referenda.rs b/src/cli/referenda.rs index 04f4416..9e2ead3 100644 --- a/src/cli/referenda.rs +++ b/src/cli/referenda.rs @@ -178,7 +178,7 @@ pub async fn handle_referenda_command( let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; match command { - ReferendaCommands::SubmitRemark { message, from, password, password_file, origin } => + ReferendaCommands::SubmitRemark { message, from, password, password_file, origin } => { submit_remark_proposal( &quantus_client, &message, @@ -188,8 +188,9 @@ pub async fn handle_referenda_command( &origin, execution_mode, ) - .await, - ReferendaCommands::Submit { preimage_hash, from, password, password_file, origin } => + .await + }, + ReferendaCommands::Submit { preimage_hash, from, password, password_file, origin } => { submit_proposal( &quantus_client, &preimage_hash, @@ -199,12 +200,14 @@ pub async fn handle_referenda_command( &origin, execution_mode, ) - .await, + .await + }, ReferendaCommands::List => list_proposals(&quantus_client).await, - ReferendaCommands::Get { index, decode } => - get_proposal_details(&quantus_client, index, decode).await, + ReferendaCommands::Get { index, decode } => { + get_proposal_details(&quantus_client, index, decode).await + }, ReferendaCommands::Status { index } => get_proposal_status(&quantus_client, index).await, - ReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => + ReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => { place_decision_deposit( &quantus_client, index, @@ -213,7 +216,8 @@ pub async fn handle_referenda_command( password_file, execution_mode, ) - .await, + .await + }, ReferendaCommands::Vote { index, aye, @@ -222,7 +226,7 @@ pub async fn handle_referenda_command( from, password, password_file, - } => + } => { vote_on_referendum( &quantus_client, index, @@ -234,8 +238,9 @@ pub async fn handle_referenda_command( password_file, execution_mode, ) - .await, - ReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => + .await + }, + ReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => { refund_submission_deposit( &quantus_client, index, @@ -244,8 +249,9 @@ pub async fn handle_referenda_command( password_file, execution_mode, ) - .await, - ReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => + .await + }, + ReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => { refund_decision_deposit( &quantus_client, index, @@ -254,7 +260,8 @@ pub async fn handle_referenda_command( password_file, execution_mode, ) - .await, + .await + }, ReferendaCommands::Config => get_config(&quantus_client).await, } } @@ -342,11 +349,12 @@ async fn submit_remark_proposal( quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Root; quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) }, - _ => + _ => { return Err(QuantusError::Generic(format!( "Invalid origin type: {}. Must be 'signed', 'none', or 'root'", origin_type - ))), + ))) + }, }; let enactment = @@ -459,11 +467,12 @@ async fn submit_proposal( quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Root; quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) }, - _ => + _ => { return Err(QuantusError::Generic(format!( "Invalid origin type: {}. Must be 'signed', 'none', or 'root'", origin_type - ))), + ))) + }, }; let enactment = @@ -724,8 +733,8 @@ async fn vote_on_referendum( let amount_value: u128 = (amount .parse::() .map_err(|_| QuantusError::Generic("Invalid amount format".to_string()))? - .max(0.0) * - 1_000_000_000_000_000_000.0) as u128; + .max(0.0) + * 1_000_000_000_000_000_000.0) as u128; // Validate conviction if conviction > 6 { diff --git a/src/cli/referenda_decode.rs b/src/cli/referenda_decode.rs index 7140fa7..7ee5aaf 100644 --- a/src/cli/referenda_decode.rs +++ b/src/cli/referenda_decode.rs @@ -22,8 +22,9 @@ pub async fn decode_preimage( let content = match preimage_result { Ok(Some(bounded_vec)) => bounded_vec.0, - Ok(None) => - return Err(QuantusError::Generic(format!("Preimage not found for hash {:?}", hash))), + Ok(None) => { + return Err(QuantusError::Generic(format!("Preimage not found for hash {:?}", hash))) + }, Err(e) => return Err(QuantusError::Generic(format!("Error fetching preimage: {:?}", e))), }; diff --git a/src/cli/reversible.rs b/src/cli/reversible.rs index 63d5ed0..52bd779 100644 --- a/src/cli/reversible.rs +++ b/src/cli/reversible.rs @@ -253,8 +253,9 @@ pub async fn handle_reversible_command( let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; match command { - ReversibleCommands::ListPending { address, from, password, password_file } => - list_pending_transactions(&quantus_client, address, from, password, password_file).await, + ReversibleCommands::ListPending { address, from, password, password_file } => { + list_pending_transactions(&quantus_client, address, from, password, password_file).await + }, ReversibleCommands::ScheduleTransfer { to, amount, from, password, password_file } => { // Parse and validate the amount let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; diff --git a/src/cli/scheduler.rs b/src/cli/scheduler.rs index e89a1cd..67ab6c2 100644 --- a/src/cli/scheduler.rs +++ b/src/cli/scheduler.rs @@ -166,7 +166,8 @@ pub async fn handle_scheduler_command( Ok(()) }, SchedulerCommands::Agenda { range } => list_agenda_range(&quantus_client, &range).await, - SchedulerCommands::ScheduleRemark { after, from } => - schedule_remark(&quantus_client, after, &from, execution_mode).await, + SchedulerCommands::ScheduleRemark { after, from } => { + schedule_remark(&quantus_client, after, &from, execution_mode).await + }, } } diff --git a/src/cli/storage.rs b/src/cli/storage.rs index 2ad0ce0..3567190 100644 --- a/src/cli/storage.rs +++ b/src/cli/storage.rs @@ -839,14 +839,18 @@ pub async fn handle_storage_command( .await } }, - StorageCommands::List { pallet, names_only } => - list_storage_items(&quantus_client, &pallet, names_only).await, - StorageCommands::ListPallets { with_counts } => - list_pallets_with_storage(&quantus_client, with_counts).await, - StorageCommands::Stats { pallet, detailed } => - show_storage_stats(&quantus_client, pallet, detailed).await, - StorageCommands::Iterate { pallet, name, limit, decode_as, block } => - iterate_storage_entries(&quantus_client, &pallet, &name, limit, decode_as, block).await, + StorageCommands::List { pallet, names_only } => { + list_storage_items(&quantus_client, &pallet, names_only).await + }, + StorageCommands::ListPallets { with_counts } => { + list_pallets_with_storage(&quantus_client, with_counts).await + }, + StorageCommands::Stats { pallet, detailed } => { + show_storage_stats(&quantus_client, pallet, detailed).await + }, + StorageCommands::Iterate { pallet, name, limit, decode_as, block } => { + iterate_storage_entries(&quantus_client, &pallet, &name, limit, decode_as, block).await + }, StorageCommands::Set { pallet, name, value, wallet, password, password_file, r#type } => { log_print!("✍️ Setting storage for {}::{}", pallet.bright_green(), name.bright_cyan()); @@ -890,10 +894,11 @@ pub async fn handle_storage_command( .map_err(|e| QuantusError::Generic(format!("Invalid hex value: {e}")))? } }, - Some(unsupported) => + Some(unsupported) => { return Err(QuantusError::Generic(format!( "Unsupported type for --type: {unsupported}" - ))), + ))) + }, }; log_verbose!("Encoded value bytes: 0x{}", hex::encode(&value_bytes).dimmed()); diff --git a/src/cli/tech_collective.rs b/src/cli/tech_collective.rs index 6234b6f..f917dd1 100644 --- a/src/cli/tech_collective.rs +++ b/src/cli/tech_collective.rs @@ -429,7 +429,7 @@ pub async fn handle_tech_collective_command( // Get actual member list match get_member_list(&quantus_client).await { - Ok(members) => + Ok(members) => { if members.is_empty() { log_print!("📭 No members in Tech Collective"); } else { @@ -443,7 +443,8 @@ pub async fn handle_tech_collective_command( member.to_quantus_ss58().bright_green() ); } - }, + } + }, Err(e) => { log_verbose!("⚠️ Failed to get member list: {:?}", e); // Fallback to member count diff --git a/src/cli/tech_referenda.rs b/src/cli/tech_referenda.rs index 6fc8860..4589c1b 100644 --- a/src/cli/tech_referenda.rs +++ b/src/cli/tech_referenda.rs @@ -192,7 +192,7 @@ pub async fn handle_tech_referenda_command( let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; match command { - TechReferendaCommands::Submit { preimage_hash, from, password, password_file } => + TechReferendaCommands::Submit { preimage_hash, from, password, password_file } => { submit_runtime_upgrade( &quantus_client, &preimage_hash, @@ -201,8 +201,9 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await, - TechReferendaCommands::SubmitWithPreimage { wasm_file, from, password, password_file } => + .await + }, + TechReferendaCommands::SubmitWithPreimage { wasm_file, from, password, password_file } => { submit_runtime_upgrade_with_preimage( &quantus_client, &wasm_file, @@ -211,12 +212,14 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await, + .await + }, TechReferendaCommands::List => list_proposals(&quantus_client).await, TechReferendaCommands::Get { index } => get_proposal_details(&quantus_client, index).await, - TechReferendaCommands::Status { index } => - get_proposal_status(&quantus_client, index).await, - TechReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => + TechReferendaCommands::Status { index } => { + get_proposal_status(&quantus_client, index).await + }, + TechReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => { place_decision_deposit( &quantus_client, index, @@ -225,17 +228,21 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await, - TechReferendaCommands::Cancel { index, from, password, password_file } => + .await + }, + TechReferendaCommands::Cancel { index, from, password, password_file } => { cancel_proposal(&quantus_client, index, &from, password, password_file, execution_mode) - .await, - TechReferendaCommands::Kill { index, from, password, password_file } => + .await + }, + TechReferendaCommands::Kill { index, from, password, password_file } => { kill_proposal(&quantus_client, index, &from, password, password_file, execution_mode) - .await, - TechReferendaCommands::Nudge { index, from, password, password_file } => + .await + }, + TechReferendaCommands::Nudge { index, from, password, password_file } => { nudge_proposal(&quantus_client, index, &from, password, password_file, execution_mode) - .await, - TechReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => + .await + }, + TechReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => { refund_submission_deposit( &quantus_client, index, @@ -244,8 +251,9 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await, - TechReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => + .await + }, + TechReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => { refund_decision_deposit( &quantus_client, index, @@ -254,7 +262,8 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await, + .await + }, TechReferendaCommands::Config => get_config(&quantus_client).await, } } diff --git a/src/cli/transfers.rs b/src/cli/transfers.rs new file mode 100644 index 0000000..eac5758 --- /dev/null +++ b/src/cli/transfers.rs @@ -0,0 +1,281 @@ +//! Privacy-preserving transfer queries via Subsquid indexer. +//! +//! This module provides commands for querying transfers using hash prefix queries, +//! which allows clients to retrieve their transactions without revealing their +//! exact addresses to the indexer. + +use crate::error::{QuantusError, Result}; +use crate::subsquid::{compute_address_hash, get_hash_prefix, SubsquidClient, TransferQueryParams}; +use crate::wallet::WalletManager; +use crate::{log_error, log_print, log_success, log_verbose}; +use clap::Subcommand; +use colored::Colorize; +use sp_core::crypto::{AccountId32, Ss58Codec}; + +/// Transfers subcommands +#[derive(Subcommand, Debug)] +pub enum TransfersCommands { + /// Query transfers for your wallet addresses using privacy-preserving hash prefix queries + Query { + /// Subsquid indexer URL (e.g., "https://indexer.quantus.com/graphql") + #[arg(long)] + subsquid_url: String, + + /// Hash prefix length in hex characters (1-64). + /// Shorter = more privacy but more noise, longer = less privacy but fewer false positives. + /// Default: 4 (1/65536 of address space per prefix) + #[arg(long, default_value = "4")] + prefix_len: usize, + + /// Only show transfers after this block number + #[arg(long)] + after_block: Option, + + /// Only show transfers before this block number + #[arg(long)] + before_block: Option, + + /// Minimum transfer amount (in smallest unit, e.g., planck) + #[arg(long)] + min_amount: Option, + + /// Maximum number of results (default: 100, max: 1000) + #[arg(long, default_value = "100")] + limit: u32, + + /// Specific wallet name to query for (if not provided, queries all wallets) + #[arg(long)] + wallet: Option, + + /// Show raw transfer data as JSON + #[arg(long)] + json: bool, + }, + + /// Compute the hash prefix for an address (for debugging/testing) + HashAddress { + /// The address to hash (SS58 format) + address: String, + + /// Prefix length to display + #[arg(long, default_value = "4")] + prefix_len: usize, + }, +} + +/// Handle transfers commands +pub async fn handle_transfers_command(cmd: TransfersCommands) -> Result<()> { + match cmd { + TransfersCommands::Query { + subsquid_url, + prefix_len, + after_block, + before_block, + min_amount, + limit, + wallet, + json, + } => { + handle_query_command( + subsquid_url, + prefix_len, + after_block, + before_block, + min_amount, + limit, + wallet, + json, + ) + .await + }, + TransfersCommands::HashAddress { address, prefix_len } => { + handle_hash_address_command(&address, prefix_len) + }, + } +} + +/// Handle the query subcommand +#[allow(clippy::too_many_arguments)] +async fn handle_query_command( + subsquid_url: String, + prefix_len: usize, + after_block: Option, + before_block: Option, + min_amount: Option, + limit: u32, + wallet_name: Option, + json_output: bool, +) -> Result<()> { + // Validate prefix length + if prefix_len == 0 || prefix_len > 64 { + return Err(QuantusError::Generic("Prefix length must be between 1 and 64".to_string())); + } + + // Load wallet addresses + let wallet_manager = WalletManager::new()?; + let wallets = wallet_manager.list_wallets()?; + + if wallets.is_empty() { + log_error!("No wallets found. Create a wallet first with 'quantus wallet create'"); + return Ok(()); + } + + // Filter to specific wallet if requested + let wallets_to_query: Vec<_> = if let Some(name) = &wallet_name { + wallets.into_iter().filter(|w| w.name == *name).collect() + } else { + wallets + }; + + if wallets_to_query.is_empty() { + log_error!("No matching wallet found"); + return Ok(()); + } + + // Convert SS58 addresses to raw account IDs + let mut raw_addresses: Vec<[u8; 32]> = Vec::new(); + for wallet in &wallets_to_query { + let account_id = AccountId32::from_ss58check(&wallet.address).map_err(|e| { + QuantusError::Generic(format!("Invalid address {}: {}", wallet.address, e)) + })?; + raw_addresses.push(account_id.into()); + } + + if !json_output { + log_print!("{}", "Privacy-Preserving Transfer Query".bright_cyan().bold()); + log_print!(""); + log_print!( + " Querying for {} wallet(s) with prefix length {}", + wallets_to_query.len().to_string().bright_yellow(), + prefix_len.to_string().bright_yellow() + ); + log_print!( + " Privacy level: ~1/{} of address space per query", + (1u64 << (prefix_len * 4)).to_string().bright_green() + ); + log_print!(""); + } + + // Create Subsquid client + let client = SubsquidClient::new(subsquid_url)?; + + // Build query params + let mut params = TransferQueryParams::new().with_limit(limit); + if let Some(block) = after_block { + params = params.with_after_block(block); + } + if let Some(block) = before_block { + params = params.with_before_block(block); + } + if let Some(amount) = min_amount { + params = params.with_min_amount(amount); + } + + // Query transfers + let transfers = + client.query_transfers_for_addresses(&raw_addresses, prefix_len, params).await?; + + if json_output { + // Output as JSON + let json = serde_json::to_string_pretty(&transfers) + .map_err(|e| QuantusError::Generic(format!("Failed to serialize transfers: {}", e)))?; + println!("{}", json); + } else { + // Display formatted output + if transfers.is_empty() { + log_print!("No transfers found for your addresses."); + } else { + log_success!("Found {} transfers:", transfers.len().to_string().bright_green()); + log_print!(""); + + for transfer in &transfers { + // Determine if this is incoming or outgoing + let our_address_hashes: std::collections::HashSet = + raw_addresses.iter().map(compute_address_hash).collect(); + + let is_incoming = our_address_hashes.contains(&transfer.to_hash); + let is_outgoing = our_address_hashes.contains(&transfer.from_hash); + + let direction = match (is_incoming, is_outgoing) { + (true, true) => "SELF".bright_blue(), + (true, false) => "IN".bright_green(), + (false, true) => "OUT".bright_red(), + (false, false) => "???".dimmed(), // Shouldn't happen + }; + + // Parse and format amount + let amount: u128 = transfer.amount.parse().unwrap_or(0); + let formatted_amount = format_planck_amount(amount); + + log_print!( + " [{}] {} | Block {} | {} | {} -> {}", + direction, + &transfer.timestamp[..19], // Truncate to YYYY-MM-DDTHH:MM:SS + transfer.block_height.to_string().bright_yellow(), + formatted_amount.bright_cyan(), + truncate_address(&transfer.from_id), + truncate_address(&transfer.to_id), + ); + + if let Some(hash) = &transfer.extrinsic_hash { + log_verbose!(" Extrinsic: {}", hash.dimmed()); + } + } + } + } + + Ok(()) +} + +/// Handle the hash-address subcommand +fn handle_hash_address_command(address: &str, prefix_len: usize) -> Result<()> { + // Parse the SS58 address + let account_id = AccountId32::from_ss58check(address) + .map_err(|e| QuantusError::Generic(format!("Invalid address: {}", e)))?; + + let raw_address: [u8; 32] = account_id.into(); + let full_hash = compute_address_hash(&raw_address); + let prefix = get_hash_prefix(&full_hash, prefix_len); + + log_print!("{}", "Address Hash Information".bright_cyan().bold()); + log_print!(""); + log_print!(" Address: {}", address.bright_yellow()); + log_print!(" Full Hash: {}", full_hash.dimmed()); + log_print!(" Prefix ({}): {}", prefix_len, prefix.bright_green().bold()); + log_print!(""); + log_print!( + " Privacy: With prefix length {}, your query will match ~1/{} of all addresses", + prefix_len, + (1u64 << (prefix_len * 4)).to_string().bright_cyan() + ); + + Ok(()) +} + +/// Format a planck amount to a human-readable string +fn format_planck_amount(planck: u128) -> String { + // Assuming 12 decimal places (standard for Substrate chains) + let decimals = 12u32; + let divisor = 10u128.pow(decimals); + let whole = planck / divisor; + let frac = planck % divisor; + + if frac == 0 { + format!("{} QTM", whole) + } else { + // Format with up to 4 decimal places + let frac_str = format!("{:012}", frac); + let trimmed = frac_str.trim_end_matches('0'); + let display_frac = if trimmed.len() > 4 { &trimmed[..4] } else { trimmed }; + format!("{}.{} QTM", whole, display_frac) + } +} + +/// Truncate an address for display +fn truncate_address(address: &str) -> String { + if address.len() > 16 { + format!("{}...{}", &address[..8], &address[address.len() - 6..]) + } else { + address.to_string() + } +} diff --git a/src/cli/treasury.rs b/src/cli/treasury.rs index e910bea..b82509a 100644 --- a/src/cli/treasury.rs +++ b/src/cli/treasury.rs @@ -132,7 +132,7 @@ pub async fn handle_treasury_command( from, password, password_file, - } => + } => { submit_spend_referendum( &quantus_client, &beneficiary, @@ -143,11 +143,13 @@ pub async fn handle_treasury_command( password_file, execution_mode, ) - .await, - TreasuryCommands::Payout { index, from, password, password_file } => + .await + }, + TreasuryCommands::Payout { index, from, password, password_file } => { payout_spend(&quantus_client, index, &from, password, password_file, execution_mode) - .await, - TreasuryCommands::CheckStatus { index, from, password, password_file } => + .await + }, + TreasuryCommands::CheckStatus { index, from, password, password_file } => { check_spend_status( &quantus_client, index, @@ -156,9 +158,10 @@ pub async fn handle_treasury_command( password_file, execution_mode, ) - .await, + .await + }, TreasuryCommands::ListSpends => list_spends(&quantus_client).await, - TreasuryCommands::SpendSudo { beneficiary, amount, from, password, password_file } => + TreasuryCommands::SpendSudo { beneficiary, amount, from, password, password_file } => { spend_sudo( &quantus_client, &beneficiary, @@ -168,7 +171,8 @@ pub async fn handle_treasury_command( password_file, execution_mode, ) - .await, + .await + }, } } diff --git a/src/cli/wallet.rs b/src/cli/wallet.rs index ee30a21..b1425d5 100644 --- a/src/cli/wallet.rs +++ b/src/cli/wallet.rs @@ -224,7 +224,7 @@ pub async fn handle_wallet_command( if all { // Show all wallets (same as list command but with different header) match wallet_manager.list_wallets() { - Ok(wallets) => + Ok(wallets) => { if wallets.is_empty() { log_print!("{}", "No wallets found.".dimmed()); } else { @@ -254,7 +254,8 @@ pub async fn handle_wallet_command( log_print!(); } } - }, + } + }, Err(e) => { log_error!("{}", format!("❌ Failed to view wallets: {e}").red()); return Err(e); @@ -445,7 +446,7 @@ pub async fn handle_wallet_command( let wallet_manager = WalletManager::new()?; match wallet_manager.list_wallets() { - Ok(wallets) => + Ok(wallets) => { if wallets.is_empty() { log_print!("{}", "No wallets found.".dimmed()); log_print!( @@ -481,7 +482,8 @@ pub async fn handle_wallet_command( "💡 Use 'quantus wallet view --name ' to see full details" .dimmed() ); - }, + } + }, Err(e) => { log_error!("{}", format!("❌ Failed to list wallets: {e}").red()); return Err(e); diff --git a/src/lib.rs b/src/lib.rs index 95389b8..01a73ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod cli; pub mod config; pub mod error; pub mod log; +pub mod subsquid; pub mod wallet; // Re-export commonly used types and functions diff --git a/src/main.rs b/src/main.rs index 2bf667a..684b112 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ mod cli; mod config; mod error; mod log; +mod subsquid; mod wallet; use cli::Commands; diff --git a/src/subsquid/client.rs b/src/subsquid/client.rs new file mode 100644 index 0000000..d0ef368 --- /dev/null +++ b/src/subsquid/client.rs @@ -0,0 +1,231 @@ +//! Subsquid GraphQL client for privacy-preserving transfer queries. + +use crate::error::{QuantusError, Result}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +use super::types::{GraphQLResponse, Transfer, TransferQueryParams, TransfersByPrefixResult}; + +/// Client for querying the Subsquid indexer. +pub struct SubsquidClient { + url: String, + http_client: Client, +} + +#[derive(Serialize)] +struct GraphQLRequest { + query: String, + variables: serde_json::Value, +} + +#[derive(Deserialize)] +struct TransfersByHashPrefixData { + #[serde(rename = "transfersByHashPrefix")] + transfers_by_hash_prefix: TransfersByPrefixResult, +} + +impl SubsquidClient { + /// Create a new Subsquid client. + /// + /// # Arguments + /// + /// * `url` - The GraphQL endpoint URL (e.g., "https://indexer.quantus.com/graphql") + pub fn new(url: String) -> Result { + let http_client = Client::builder() + .build() + .map_err(|e| QuantusError::Generic(format!("Failed to create HTTP client: {}", e)))?; + + Ok(Self { url, http_client }) + } + + /// Query transfers by hash prefixes. + /// + /// This method allows privacy-preserving queries by matching against + /// blake3 hash prefixes of addresses rather than the addresses themselves. + /// + /// # Arguments + /// + /// * `to_prefixes` - Hash prefixes for destination addresses (OR logic) + /// * `from_prefixes` - Hash prefixes for source addresses (OR logic) + /// * `params` - Additional query parameters (block range, amount filters, pagination) + /// + /// # Returns + /// + /// A list of matching transfers. Returns an error if too many results match. + pub async fn query_transfers_by_prefix( + &self, + to_prefixes: Option>, + from_prefixes: Option>, + params: TransferQueryParams, + ) -> Result> { + // Build the GraphQL query + let query = r#" + query TransfersByHashPrefix($input: TransfersByPrefixInput!) { + transfersByHashPrefix(input: $input) { + transfers { + id + blockId + blockHeight + timestamp + extrinsicHash + fromId + toId + amount + fee + fromHash + toHash + } + totalCount + } + } + "#; + + // Build input variables + let mut input = serde_json::json!({ + "limit": params.limit, + "offset": params.offset, + }); + + if let Some(prefixes) = to_prefixes { + input["toHashPrefixes"] = serde_json::json!(prefixes); + } + + if let Some(prefixes) = from_prefixes { + input["fromHashPrefixes"] = serde_json::json!(prefixes); + } + + if let Some(block) = params.after_block { + input["afterBlock"] = serde_json::json!(block); + } + + if let Some(block) = params.before_block { + input["beforeBlock"] = serde_json::json!(block); + } + + if let Some(amount) = params.min_amount { + input["minAmount"] = serde_json::json!(amount.to_string()); + } + + if let Some(amount) = params.max_amount { + input["maxAmount"] = serde_json::json!(amount.to_string()); + } + + let request = GraphQLRequest { + query: query.to_string(), + variables: serde_json::json!({ "input": input }), + }; + + // Send request + let response = self + .http_client + .post(&self.url) + .json(&request) + .send() + .await + .map_err(|e| QuantusError::Generic(format!("Failed to send request: {}", e)))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(QuantusError::Generic(format!( + "Subsquid request failed with status {}: {}", + status, body + ))); + } + + let graphql_response: GraphQLResponse = response + .json() + .await + .map_err(|e| QuantusError::Generic(format!("Failed to parse response: {}", e)))?; + + // Check for GraphQL errors + if let Some(errors) = graphql_response.errors { + let error_messages: Vec = errors.iter().map(|e| e.message.clone()).collect(); + return Err(QuantusError::Generic(format!( + "GraphQL errors: {}", + error_messages.join("; ") + ))); + } + + // Extract transfers + let data = graphql_response + .data + .ok_or_else(|| QuantusError::Generic("No data in response".to_string()))?; + + Ok(data.transfers_by_hash_prefix.transfers) + } + + /// Query transfers for a set of addresses using privacy-preserving hash prefixes. + /// + /// This is a convenience method that: + /// 1. Computes hash prefixes for all provided addresses + /// 2. Queries the indexer with those prefixes + /// 3. Filters results locally to only include transfers involving the exact addresses + /// + /// # Arguments + /// + /// * `addresses` - Raw 32-byte account IDs to query for + /// * `prefix_len` - Length of hash prefix to use (shorter = more privacy, more noise) + /// * `params` - Additional query parameters + /// + /// # Returns + /// + /// Transfers involving any of the provided addresses (filtered locally for exact matches) + pub async fn query_transfers_for_addresses( + &self, + addresses: &[[u8; 32]], + prefix_len: usize, + params: TransferQueryParams, + ) -> Result> { + use super::hash::{compute_address_hash, get_hash_prefix}; + use std::collections::HashSet; + + if addresses.is_empty() { + return Ok(vec![]); + } + + // Compute full hashes and prefixes for all addresses + let address_hashes: HashSet = addresses.iter().map(compute_address_hash).collect(); + + let prefixes: Vec = address_hashes + .iter() + .map(|h| get_hash_prefix(h, prefix_len)) + .collect::>() + .into_iter() + .collect(); + + // Query with prefixes (for both to and from) + let transfers = self + .query_transfers_by_prefix(Some(prefixes.clone()), Some(prefixes), params) + .await?; + + // Filter locally to only include exact matches + let filtered: Vec = transfers + .into_iter() + .filter(|t| { + address_hashes.contains(&t.to_hash) || address_hashes.contains(&t.from_hash) + }) + .collect(); + + Ok(filtered) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_transfer_query_params_builder() { + let params = TransferQueryParams::new() + .with_limit(50) + .with_offset(10) + .with_after_block(1000) + .with_before_block(2000); + + assert_eq!(params.limit, 50); + assert_eq!(params.offset, 10); + assert_eq!(params.after_block, Some(1000)); + assert_eq!(params.before_block, Some(2000)); + } +} diff --git a/src/subsquid/hash.rs b/src/subsquid/hash.rs new file mode 100644 index 0000000..b8bad59 --- /dev/null +++ b/src/subsquid/hash.rs @@ -0,0 +1,175 @@ +//! Hash utilities for privacy-preserving queries. +//! +//! Uses blake3 to compute address hashes that match the Subsquid indexer. + +/// Compute blake3 hash of raw address bytes and return as hex string. +/// +/// This matches the hash computation done by the Subsquid indexer. +/// +/// # Arguments +/// +/// * `raw_address` - The raw 32-byte account ID +/// +/// # Returns +/// +/// The blake3 hash as a 64-character hex string +pub fn compute_address_hash(raw_address: &[u8; 32]) -> String { + let hash = blake3::hash(raw_address); + hex::encode(hash.as_bytes()) +} + +/// Get a prefix of the specified length from a hash. +/// +/// # Arguments +/// +/// * `hash` - The full hash as a hex string +/// * `prefix_len` - The number of hex characters to include in the prefix +/// +/// # Returns +/// +/// The prefix as a hex string +pub fn get_hash_prefix(hash: &str, prefix_len: usize) -> String { + hash.chars().take(prefix_len).collect() +} + +/// Compute the hash prefix for a raw address with the specified prefix length. +/// +/// Convenience function combining `compute_address_hash` and `get_hash_prefix`. +pub fn compute_address_prefix(raw_address: &[u8; 32], prefix_len: usize) -> String { + let hash = compute_address_hash(raw_address); + get_hash_prefix(&hash, prefix_len) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compute_address_hash() { + let address = [0u8; 32]; + let hash = compute_address_hash(&address); + + // blake3 of 32 zero bytes should produce a consistent hash + assert_eq!(hash.len(), 64); // blake3 produces 256-bit = 64 hex chars + + // Verify it's valid hex + assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn test_get_hash_prefix() { + let hash = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; + + assert_eq!(get_hash_prefix(hash, 2), "ab"); + assert_eq!(get_hash_prefix(hash, 4), "abcd"); + assert_eq!(get_hash_prefix(hash, 8), "abcdef12"); + } + + #[test] + fn test_get_hash_prefix_edge_cases() { + let hash = "abcd"; + + // Empty prefix + assert_eq!(get_hash_prefix(hash, 0), ""); + + // Full hash as prefix + assert_eq!(get_hash_prefix(hash, 4), "abcd"); + + // Prefix longer than hash - returns full hash + assert_eq!(get_hash_prefix(hash, 10), "abcd"); + } + + #[test] + fn test_compute_address_prefix() { + let address = [1u8; 32]; + let prefix = compute_address_prefix(&address, 4); + + assert_eq!(prefix.len(), 4); + assert!(prefix.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn test_hash_consistency() { + // Same input should always produce same output + let address = [42u8; 32]; + let hash1 = compute_address_hash(&address); + let hash2 = compute_address_hash(&address); + + assert_eq!(hash1, hash2); + } + + #[test] + fn test_different_addresses_different_hashes() { + let address1 = [1u8; 32]; + let address2 = [2u8; 32]; + + let hash1 = compute_address_hash(&address1); + let hash2 = compute_address_hash(&address2); + + assert_ne!(hash1, hash2); + } + + #[test] + fn test_all_zeros() { + let address = [0u8; 32]; + let hash = compute_address_hash(&address); + + // Known hash for 32 zero bytes (blake3) + // This value must match the TypeScript implementation + assert_eq!(hash, "2ada83c1819a5372dae1238fc1ded123c8104fdaa15862aaee69428a1820fcda"); + } + + #[test] + fn test_all_ones() { + let address = [0xffu8; 32]; + let hash = compute_address_hash(&address); + + // Known hash for 32 0xff bytes (blake3) + // This value must match the TypeScript implementation + assert_eq!(hash, "9b34f060fbc0f0aa11f150e26519deff613277b60656f0f8356ed2261505f5c5"); + } + + #[test] + fn test_sequential_bytes() { + let mut address = [0u8; 32]; + for (i, byte) in address.iter_mut().enumerate() { + *byte = i as u8; + } + let hash = compute_address_hash(&address); + + // Known hash for sequential bytes 0-31 (blake3) + // This value must match the TypeScript implementation + assert_eq!(hash, "e528e95798037df410543d9f31e396ecdd458d71b157d6014398bae32fb56c65"); + } + + /// Test vectors that can be used to verify cross-implementation consistency + /// between Rust and TypeScript + #[test] + fn print_test_vectors() { + let test_cases: Vec<(&str, [u8; 32])> = vec![ + ("zero_bytes", [0u8; 32]), + ("ones_bytes", [0xffu8; 32]), + ("sequential", { + let mut arr = [0u8; 32]; + for (i, byte) in arr.iter_mut().enumerate() { + *byte = i as u8; + } + arr + }), + ("all_42s", [42u8; 32]), + ]; + + println!("\n=== Test Vectors for Cross-Implementation Testing ==="); + println!("Use these to verify TypeScript implementation matches:\n"); + + for (name, address) in test_cases { + let hash = compute_address_hash(&address); + println!("{}:", name); + println!(" input: {}", hex::encode(address)); + println!(" hash: {}", hash); + println!(" prefix4: {}", get_hash_prefix(&hash, 4)); + println!(" prefix8: {}", get_hash_prefix(&hash, 8)); + println!(); + } + } +} diff --git a/src/subsquid/mod.rs b/src/subsquid/mod.rs new file mode 100644 index 0000000..1ad91ea --- /dev/null +++ b/src/subsquid/mod.rs @@ -0,0 +1,13 @@ +//! Subsquid indexer client module for privacy-preserving queries. +//! +//! This module provides a client for querying the Subsquid indexer using +//! hash prefix queries, which allows clients to retrieve their transactions +//! without revealing their exact addresses to the indexer. + +mod client; +mod hash; +mod types; + +pub use client::SubsquidClient; +pub use hash::{compute_address_hash, get_hash_prefix}; +pub use types::*; diff --git a/src/subsquid/types.rs b/src/subsquid/types.rs new file mode 100644 index 0000000..049219e --- /dev/null +++ b/src/subsquid/types.rs @@ -0,0 +1,263 @@ +//! Types for Subsquid API responses. + +use serde::{Deserialize, Serialize}; + +/// A transfer as returned by the Subsquid indexer. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Transfer { + /// Unique identifier + pub id: String, + + /// Block ID + pub block_id: String, + + /// Block height + pub block_height: i64, + + /// Timestamp of the transfer + pub timestamp: String, + + /// Extrinsic hash (if available) + pub extrinsic_hash: Option, + + /// Sender address (SS58 format) + pub from_id: String, + + /// Recipient address (SS58 format) + pub to_id: String, + + /// Transfer amount (as string to handle large numbers) + pub amount: String, + + /// Transaction fee + pub fee: String, + + /// Blake3 hash of the sender's raw address + pub from_hash: String, + + /// Blake3 hash of the recipient's raw address + pub to_hash: String, +} + +/// Result from a prefix query. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransfersByPrefixResult { + /// Matching transfers + pub transfers: Vec, + + /// Total count of matches + pub total_count: i64, +} + +/// GraphQL response wrapper. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GraphQLResponse { + pub data: Option, + pub errors: Option>, +} + +/// GraphQL error. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GraphQLError { + pub message: String, + pub locations: Option>, + pub path: Option>, +} + +/// GraphQL error location. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GraphQLErrorLocation { + pub line: i64, + pub column: i64, +} + +/// Query parameters for transfer prefix queries. +#[derive(Debug, Clone, Default)] +pub struct TransferQueryParams { + /// Minimum block number (inclusive) + pub after_block: Option, + + /// Maximum block number (inclusive) + pub before_block: Option, + + /// Minimum transfer amount + pub min_amount: Option, + + /// Maximum transfer amount + pub max_amount: Option, + + /// Maximum number of results + pub limit: u32, + + /// Offset for pagination + pub offset: u32, +} + +impl TransferQueryParams { + pub fn new() -> Self { + Self { limit: 100, offset: 0, ..Default::default() } + } + + pub fn with_limit(mut self, limit: u32) -> Self { + self.limit = limit; + self + } + + pub fn with_offset(mut self, offset: u32) -> Self { + self.offset = offset; + self + } + + pub fn with_after_block(mut self, block: u32) -> Self { + self.after_block = Some(block); + self + } + + pub fn with_before_block(mut self, block: u32) -> Self { + self.before_block = Some(block); + self + } + + pub fn with_min_amount(mut self, amount: u128) -> Self { + self.min_amount = Some(amount); + self + } + + pub fn with_max_amount(mut self, amount: u128) -> Self { + self.max_amount = Some(amount); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_transfer_query_params_default() { + let params = TransferQueryParams::default(); + assert_eq!(params.limit, 0); + assert_eq!(params.offset, 0); + assert!(params.after_block.is_none()); + assert!(params.before_block.is_none()); + assert!(params.min_amount.is_none()); + assert!(params.max_amount.is_none()); + } + + #[test] + fn test_transfer_query_params_new() { + let params = TransferQueryParams::new(); + assert_eq!(params.limit, 100); + assert_eq!(params.offset, 0); + } + + #[test] + fn test_transfer_query_params_builder() { + let params = TransferQueryParams::new() + .with_limit(50) + .with_offset(10) + .with_after_block(1000) + .with_before_block(2000) + .with_min_amount(1_000_000) + .with_max_amount(10_000_000); + + assert_eq!(params.limit, 50); + assert_eq!(params.offset, 10); + assert_eq!(params.after_block, Some(1000)); + assert_eq!(params.before_block, Some(2000)); + assert_eq!(params.min_amount, Some(1_000_000)); + assert_eq!(params.max_amount, Some(10_000_000)); + } + + #[test] + fn test_transfer_deserialization() { + let json = r#"{ + "id": "transfer-123", + "blockId": "block-456", + "blockHeight": 12345, + "timestamp": "2024-01-15T12:30:00Z", + "extrinsicHash": "0xabcd1234", + "fromId": "qzAlice123", + "toId": "qzBob456", + "amount": "1000000000000", + "fee": "1000000", + "fromHash": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", + "toHash": "5678efgh5678efgh5678efgh5678efgh5678efgh5678efgh5678efgh5678efgh" + }"#; + + let transfer: Transfer = serde_json::from_str(json).expect("should deserialize"); + + assert_eq!(transfer.id, "transfer-123"); + assert_eq!(transfer.block_id, "block-456"); + assert_eq!(transfer.block_height, 12345); + assert_eq!(transfer.timestamp, "2024-01-15T12:30:00Z"); + assert_eq!(transfer.extrinsic_hash, Some("0xabcd1234".to_string())); + assert_eq!(transfer.from_id, "qzAlice123"); + assert_eq!(transfer.to_id, "qzBob456"); + assert_eq!(transfer.amount, "1000000000000"); + assert_eq!(transfer.fee, "1000000"); + } + + #[test] + fn test_transfer_deserialization_null_extrinsic_hash() { + let json = r#"{ + "id": "transfer-123", + "blockId": "block-456", + "blockHeight": 12345, + "timestamp": "2024-01-15T12:30:00Z", + "extrinsicHash": null, + "fromId": "qzAlice123", + "toId": "qzBob456", + "amount": "1000000000000", + "fee": "1000000", + "fromHash": "abcd1234", + "toHash": "5678efgh" + }"#; + + let transfer: Transfer = serde_json::from_str(json).expect("should deserialize"); + assert!(transfer.extrinsic_hash.is_none()); + } + + #[test] + fn test_graphql_response_with_data() { + let json = r#"{ + "data": { + "transfers": [], + "totalCount": 0 + } + }"#; + + let response: GraphQLResponse = + serde_json::from_str(json).expect("should deserialize"); + + assert!(response.data.is_some()); + assert!(response.errors.is_none()); + assert_eq!(response.data.unwrap().total_count, 0); + } + + #[test] + fn test_graphql_response_with_error() { + let json = r#"{ + "data": null, + "errors": [ + { + "message": "Query returned too many results", + "locations": [{"line": 1, "column": 1}], + "path": ["transfersByHashPrefix"] + } + ] + }"#; + + let response: GraphQLResponse = + serde_json::from_str(json).expect("should deserialize"); + + assert!(response.data.is_none()); + assert!(response.errors.is_some()); + + let errors = response.errors.unwrap(); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].message, "Query returned too many results"); + } +} From af5a49b166e7d8463842913caa4a843bd2f08f29 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 26 Jan 2026 14:40:03 +0800 Subject: [PATCH 2/7] DEV --- src/cli/transfers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/transfers.rs b/src/cli/transfers.rs index eac5758..d4c3872 100644 --- a/src/cli/transfers.rs +++ b/src/cli/transfers.rs @@ -261,13 +261,13 @@ fn format_planck_amount(planck: u128) -> String { let frac = planck % divisor; if frac == 0 { - format!("{} QTM", whole) + format!("{} DEV", whole) } else { // Format with up to 4 decimal places let frac_str = format!("{:012}", frac); let trimmed = frac_str.trim_end_matches('0'); let display_frac = if trimmed.len() > 4 { &trimmed[..4] } else { trimmed }; - format!("{}.{} QTM", whole, display_frac) + format!("{}.{} DEV", whole, display_frac) } } From e80e8501282eee15b0b39bde222985a4888f383d Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 26 Jan 2026 14:54:28 +0800 Subject: [PATCH 3/7] remove dumb tests --- src/subsquid/hash.rs | 141 +++++++++---------------------------------- 1 file changed, 30 insertions(+), 111 deletions(-) diff --git a/src/subsquid/hash.rs b/src/subsquid/hash.rs index b8bad59..e7ebb25 100644 --- a/src/subsquid/hash.rs +++ b/src/subsquid/hash.rs @@ -44,132 +44,51 @@ pub fn compute_address_prefix(raw_address: &[u8; 32], prefix_len: usize) -> Stri mod tests { use super::*; - #[test] - fn test_compute_address_hash() { - let address = [0u8; 32]; - let hash = compute_address_hash(&address); - - // blake3 of 32 zero bytes should produce a consistent hash - assert_eq!(hash.len(), 64); // blake3 produces 256-bit = 64 hex chars - - // Verify it's valid hex - assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); - } - - #[test] - fn test_get_hash_prefix() { - let hash = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; - - assert_eq!(get_hash_prefix(hash, 2), "ab"); - assert_eq!(get_hash_prefix(hash, 4), "abcd"); - assert_eq!(get_hash_prefix(hash, 8), "abcdef12"); - } + // Known test vectors - these values are verified against the TypeScript implementation + const ZERO_BYTES_HASH: &str = + "2ada83c1819a5372dae1238fc1ded123c8104fdaa15862aaee69428a1820fcda"; + const ONES_BYTES_HASH: &str = + "9b34f060fbc0f0aa11f150e26519deff613277b60656f0f8356ed2261505f5c5"; + const SEQUENTIAL_BYTES_HASH: &str = + "e528e95798037df410543d9f31e396ecdd458d71b157d6014398bae32fb56c65"; #[test] - fn test_get_hash_prefix_edge_cases() { - let hash = "abcd"; - - // Empty prefix - assert_eq!(get_hash_prefix(hash, 0), ""); + fn test_known_hash_vectors() { + // These test vectors ensure Rust and TypeScript produce identical hashes + assert_eq!(compute_address_hash(&[0u8; 32]), ZERO_BYTES_HASH); + assert_eq!(compute_address_hash(&[0xffu8; 32]), ONES_BYTES_HASH); - // Full hash as prefix - assert_eq!(get_hash_prefix(hash, 4), "abcd"); - - // Prefix longer than hash - returns full hash - assert_eq!(get_hash_prefix(hash, 10), "abcd"); + let mut sequential = [0u8; 32]; + for (i, byte) in sequential.iter_mut().enumerate() { + *byte = i as u8; + } + assert_eq!(compute_address_hash(&sequential), SEQUENTIAL_BYTES_HASH); } #[test] - fn test_compute_address_prefix() { - let address = [1u8; 32]; - let prefix = compute_address_prefix(&address, 4); - - assert_eq!(prefix.len(), 4); - assert!(prefix.chars().all(|c| c.is_ascii_hexdigit())); + fn test_hash_format() { + let hash = compute_address_hash(&[0u8; 32]); + assert_eq!(hash.len(), 64); + assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); } #[test] - fn test_hash_consistency() { - // Same input should always produce same output + fn test_hash_determinism() { let address = [42u8; 32]; - let hash1 = compute_address_hash(&address); - let hash2 = compute_address_hash(&address); - - assert_eq!(hash1, hash2); - } - - #[test] - fn test_different_addresses_different_hashes() { - let address1 = [1u8; 32]; - let address2 = [2u8; 32]; - - let hash1 = compute_address_hash(&address1); - let hash2 = compute_address_hash(&address2); - - assert_ne!(hash1, hash2); - } - - #[test] - fn test_all_zeros() { - let address = [0u8; 32]; - let hash = compute_address_hash(&address); - - // Known hash for 32 zero bytes (blake3) - // This value must match the TypeScript implementation - assert_eq!(hash, "2ada83c1819a5372dae1238fc1ded123c8104fdaa15862aaee69428a1820fcda"); - } - - #[test] - fn test_all_ones() { - let address = [0xffu8; 32]; - let hash = compute_address_hash(&address); - - // Known hash for 32 0xff bytes (blake3) - // This value must match the TypeScript implementation - assert_eq!(hash, "9b34f060fbc0f0aa11f150e26519deff613277b60656f0f8356ed2261505f5c5"); + assert_eq!(compute_address_hash(&address), compute_address_hash(&address)); } #[test] - fn test_sequential_bytes() { - let mut address = [0u8; 32]; - for (i, byte) in address.iter_mut().enumerate() { - *byte = i as u8; - } - let hash = compute_address_hash(&address); - - // Known hash for sequential bytes 0-31 (blake3) - // This value must match the TypeScript implementation - assert_eq!(hash, "e528e95798037df410543d9f31e396ecdd458d71b157d6014398bae32fb56c65"); + fn test_different_inputs_different_hashes() { + assert_ne!(compute_address_hash(&[1u8; 32]), compute_address_hash(&[2u8; 32])); } - /// Test vectors that can be used to verify cross-implementation consistency - /// between Rust and TypeScript #[test] - fn print_test_vectors() { - let test_cases: Vec<(&str, [u8; 32])> = vec![ - ("zero_bytes", [0u8; 32]), - ("ones_bytes", [0xffu8; 32]), - ("sequential", { - let mut arr = [0u8; 32]; - for (i, byte) in arr.iter_mut().enumerate() { - *byte = i as u8; - } - arr - }), - ("all_42s", [42u8; 32]), - ]; - - println!("\n=== Test Vectors for Cross-Implementation Testing ==="); - println!("Use these to verify TypeScript implementation matches:\n"); - - for (name, address) in test_cases { - let hash = compute_address_hash(&address); - println!("{}:", name); - println!(" input: {}", hex::encode(address)); - println!(" hash: {}", hash); - println!(" prefix4: {}", get_hash_prefix(&hash, 4)); - println!(" prefix8: {}", get_hash_prefix(&hash, 8)); - println!(); - } + fn test_get_hash_prefix() { + let hash = "abcdef1234567890"; + assert_eq!(get_hash_prefix(hash, 0), ""); + assert_eq!(get_hash_prefix(hash, 2), "ab"); + assert_eq!(get_hash_prefix(hash, 4), "abcd"); + assert_eq!(get_hash_prefix(hash, 100), hash); // longer than input returns full string } } From 8042cd90e0f706d6f81fde32b7f2d1dcaf94c134 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 26 Jan 2026 14:59:18 +0800 Subject: [PATCH 4/7] fmt? --- src/chain/quantus_subxt.rs | 4 +- src/cli/batch.rs | 10 ++--- src/cli/block.rs | 14 +++--- src/cli/common.rs | 15 +++---- src/cli/generic_call.rs | 45 ++++++++------------ src/cli/mod.rs | 85 +++++++++++++++---------------------- src/cli/referenda.rs | 49 +++++++++------------ src/cli/referenda_decode.rs | 5 +-- src/cli/reversible.rs | 5 +-- src/cli/scheduler.rs | 5 +-- src/cli/storage.rs | 25 +++++------ src/cli/tech_collective.rs | 5 +-- src/cli/tech_referenda.rs | 45 ++++++++------------ src/cli/transfers.rs | 20 ++++----- src/cli/treasury.rs | 20 ++++----- src/cli/wallet.rs | 10 ++--- 16 files changed, 148 insertions(+), 214 deletions(-) diff --git a/src/chain/quantus_subxt.rs b/src/chain/quantus_subxt.rs index d2c9f6d..a94a0d2 100644 --- a/src/chain/quantus_subxt.rs +++ b/src/chain/quantus_subxt.rs @@ -1995,8 +1995,8 @@ pub mod api { .only_these_pallets(&PALLETS) .only_these_runtime_apis(&RUNTIME_APIS) .hash(); - runtime_metadata_hash - == [ + runtime_metadata_hash == + [ 194u8, 46u8, 30u8, 103u8, 67u8, 25u8, 224u8, 42u8, 104u8, 224u8, 105u8, 213u8, 149u8, 58u8, 199u8, 151u8, 221u8, 215u8, 141u8, 247u8, 109u8, 85u8, 204u8, 202u8, 96u8, 104u8, 173u8, 94u8, 198u8, 124u8, 113u8, 174u8, diff --git a/src/cli/batch.rs b/src/cli/batch.rs index 0449979..b1b84c8 100644 --- a/src/cli/batch.rs +++ b/src/cli/batch.rs @@ -75,7 +75,7 @@ pub async fn handle_batch_command( count, to, amount, - } => { + } => handle_batch_send_command( from, node_url, @@ -88,11 +88,9 @@ pub async fn handle_batch_command( amount, execution_mode, ) - .await - }, - BatchCommands::Config { limits, info } => { - handle_batch_config_command(node_url, limits, info).await - }, + .await, + BatchCommands::Config { limits, info } => + handle_batch_config_command(node_url, limits, info).await, } } diff --git a/src/cli/block.rs b/src/cli/block.rs index b085592..a5c6b1c 100644 --- a/src/cli/block.rs +++ b/src/cli/block.rs @@ -79,7 +79,7 @@ pub async fn handle_block_command( extrinsics_details, events, all, - } => { + } => handle_block_analyze_command( number, hash, @@ -91,11 +91,9 @@ pub async fn handle_block_command( all, node_url, ) - .await - }, - BlockCommands::List { start, end, step } => { - handle_block_list_command(start, end, step, node_url).await - }, + .await, + BlockCommands::List { start, end, step } => + handle_block_list_command(start, end, step, node_url).await, } } @@ -140,9 +138,7 @@ async fn handle_block_analyze_command( })?; (block_num, hash) } else { - return Err(QuantusError::Generic( - "Must specify --number, --hash, or --latest".to_string(), - )); + return Err(QuantusError::Generic("Must specify --number, --hash, or --latest".to_string())); }; log_print!("📦 Block #{} - {:#x}", block_number, block_hash); diff --git a/src/cli/common.rs b/src/cli/common.rs index 69a69d3..32cda72 100644 --- a/src/cli/common.rs +++ b/src/cli/common.rs @@ -249,11 +249,11 @@ where let error_msg = format!("{e:?}"); // Check if it's a retryable error - let is_retryable = error_msg.contains("Priority is too low") - || error_msg.contains("Transaction is outdated") - || error_msg.contains("Transaction is temporarily banned") - || error_msg.contains("Transaction has a bad signature") - || error_msg.contains("Invalid Transaction"); + let is_retryable = error_msg.contains("Priority is too low") || + error_msg.contains("Transaction is outdated") || + error_msg.contains("Transaction is temporarily banned") || + error_msg.contains("Transaction has a bad signature") || + error_msg.contains("Invalid Transaction"); if is_retryable && attempt < 5 { log_verbose!( @@ -417,11 +417,10 @@ async fn wait_tx_inclusion( crate::log_verbose!(" Transaction status: {:?} (elapsed: {}s)", status, elapsed_secs); match status { - TxStatus::Validated => { + TxStatus::Validated => if let Some(ref pb) = spinner { pb.set_message(format!("Transaction validated ✓ ({}s)", elapsed_secs)); - } - }, + }, TxStatus::InBestBlock(tx_in_block) => { let block_hash = tx_in_block.block_hash(); crate::log_verbose!(" Transaction included in block: {:?}", block_hash); diff --git a/src/cli/generic_call.rs b/src/cli/generic_call.rs index a33f1e6..fe2049a 100644 --- a/src/cli/generic_call.rs +++ b/src/cli/generic_call.rs @@ -53,7 +53,7 @@ pub async fn execute_generic_call( let tx_hash = match (pallet, call) { // Balances pallet calls - ("Balances", "transfer_allow_death") => { + ("Balances", "transfer_allow_death") => submit_balance_transfer( quantus_client, from_keypair, @@ -62,9 +62,8 @@ pub async fn execute_generic_call( tip_amount, execution_mode, ) - .await? - }, - ("Balances", "transfer_keep_alive") => { + .await?, + ("Balances", "transfer_keep_alive") => submit_balance_transfer( quantus_client, from_keypair, @@ -73,48 +72,40 @@ pub async fn execute_generic_call( tip_amount, execution_mode, ) - .await? - }, + .await?, // System pallet calls - ("System", "remark") => { + ("System", "remark") => submit_system_remark(quantus_client, from_keypair, &args, tip_amount, execution_mode) - .await? - }, + .await?, // Sudo pallet calls ("Sudo", "sudo") => submit_sudo_call(quantus_client, from_keypair, &args).await?, // TechCollective pallet calls - ("TechCollective", "add_member") => { + ("TechCollective", "add_member") => submit_tech_collective_add_member(quantus_client, from_keypair, &args, execution_mode) - .await? - }, - ("TechCollective", "remove_member") => { + .await?, + ("TechCollective", "remove_member") => submit_tech_collective_remove_member( quantus_client, from_keypair, &args, execution_mode, ) - .await? - }, - ("TechCollective", "vote") => { - submit_tech_collective_vote(quantus_client, from_keypair, &args, execution_mode).await? - }, + .await?, + ("TechCollective", "vote") => + submit_tech_collective_vote(quantus_client, from_keypair, &args, execution_mode).await?, // ReversibleTransfers pallet calls - ("ReversibleTransfers", "schedule_transfer") => { - submit_reversible_transfer(quantus_client, from_keypair, &args, execution_mode).await? - }, + ("ReversibleTransfers", "schedule_transfer") => + submit_reversible_transfer(quantus_client, from_keypair, &args, execution_mode).await?, // Scheduler pallet calls - ("Scheduler", "schedule") => { - submit_scheduler_schedule(quantus_client, from_keypair, &args).await? - }, - ("Scheduler", "cancel") => { - submit_scheduler_cancel(quantus_client, from_keypair, &args).await? - }, + ("Scheduler", "schedule") => + submit_scheduler_schedule(quantus_client, from_keypair, &args).await?, + ("Scheduler", "cancel") => + submit_scheduler_cancel(quantus_client, from_keypair, &args).await?, // Generic fallback for unknown calls (_, _) => { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index fcac116..ccceb19 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -253,7 +253,7 @@ pub async fn execute_command( ) -> crate::error::Result<()> { match command { Commands::Wallet(wallet_cmd) => wallet::handle_wallet_command(wallet_cmd, node_url).await, - Commands::Send { from, to, amount, password, password_file, tip, nonce } => { + Commands::Send { from, to, amount, password, password_file, tip, nonce } => send::handle_send_command( from, to, @@ -265,57 +265,43 @@ pub async fn execute_command( nonce, execution_mode, ) - .await - }, - Commands::Batch(batch_cmd) => { - batch::handle_batch_command(batch_cmd, node_url, execution_mode).await - }, - Commands::Reversible(reversible_cmd) => { - reversible::handle_reversible_command(reversible_cmd, node_url, execution_mode).await - }, - Commands::HighSecurity(hs_cmd) => { - high_security::handle_high_security_command(hs_cmd, node_url, execution_mode).await - }, - Commands::Recovery(recovery_cmd) => { - recovery::handle_recovery_command(recovery_cmd, node_url, execution_mode).await - }, - Commands::Scheduler(scheduler_cmd) => { - scheduler::handle_scheduler_command(scheduler_cmd, node_url, execution_mode).await - }, - Commands::Storage(storage_cmd) => { - storage::handle_storage_command(storage_cmd, node_url, execution_mode).await - }, - Commands::TechCollective(tech_collective_cmd) => { + .await, + Commands::Batch(batch_cmd) => + batch::handle_batch_command(batch_cmd, node_url, execution_mode).await, + Commands::Reversible(reversible_cmd) => + reversible::handle_reversible_command(reversible_cmd, node_url, execution_mode).await, + Commands::HighSecurity(hs_cmd) => + high_security::handle_high_security_command(hs_cmd, node_url, execution_mode).await, + Commands::Recovery(recovery_cmd) => + recovery::handle_recovery_command(recovery_cmd, node_url, execution_mode).await, + Commands::Scheduler(scheduler_cmd) => + scheduler::handle_scheduler_command(scheduler_cmd, node_url, execution_mode).await, + Commands::Storage(storage_cmd) => + storage::handle_storage_command(storage_cmd, node_url, execution_mode).await, + Commands::TechCollective(tech_collective_cmd) => tech_collective::handle_tech_collective_command( tech_collective_cmd, node_url, execution_mode, ) - .await - }, - Commands::Preimage(preimage_cmd) => { - preimage::handle_preimage_command(preimage_cmd, node_url, execution_mode).await - }, - Commands::TechReferenda(tech_referenda_cmd) => { + .await, + Commands::Preimage(preimage_cmd) => + preimage::handle_preimage_command(preimage_cmd, node_url, execution_mode).await, + Commands::TechReferenda(tech_referenda_cmd) => tech_referenda::handle_tech_referenda_command( tech_referenda_cmd, node_url, execution_mode, ) - .await - }, - Commands::Referenda(referenda_cmd) => { - referenda::handle_referenda_command(referenda_cmd, node_url, execution_mode).await - }, - Commands::Treasury(treasury_cmd) => { - treasury::handle_treasury_command(treasury_cmd, node_url, execution_mode).await - }, - Commands::Transfers(transfers_cmd) => { - transfers::handle_transfers_command(transfers_cmd).await - }, - Commands::Runtime(runtime_cmd) => { - runtime::handle_runtime_command(runtime_cmd, node_url, execution_mode).await - }, + .await, + Commands::Referenda(referenda_cmd) => + referenda::handle_referenda_command(referenda_cmd, node_url, execution_mode).await, + Commands::Treasury(treasury_cmd) => + treasury::handle_treasury_command(treasury_cmd, node_url, execution_mode).await, + Commands::Transfers(transfers_cmd) => + transfers::handle_transfers_command(transfers_cmd).await, + Commands::Runtime(runtime_cmd) => + runtime::handle_runtime_command(runtime_cmd, node_url, execution_mode).await, Commands::Call { pallet, call, @@ -326,7 +312,7 @@ pub async fn execute_command( tip, offline, call_data_only, - } => { + } => handle_generic_call_command( pallet, call, @@ -340,8 +326,7 @@ pub async fn execute_command( node_url, execution_mode, ) - .await - }, + .await, Commands::Balance { address } => { let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; @@ -361,12 +346,11 @@ pub async fn execute_command( Ok(()) }, }, - Commands::Events { block, block_hash, latest: _, finalized, pallet, raw, no_decode } => { + Commands::Events { block, block_hash, latest: _, finalized, pallet, raw, no_decode } => events::handle_events_command( block, block_hash, finalized, pallet, raw, !no_decode, node_url, ) - .await - }, + .await, Commands::System { runtime, metadata, rpc_methods } => { if runtime || metadata || rpc_methods { system::handle_system_extended_command( @@ -381,9 +365,8 @@ pub async fn execute_command( system::handle_system_command(node_url).await } }, - Commands::Metadata { no_docs, stats_only, pallet } => { - metadata::handle_metadata_command(node_url, no_docs, stats_only, pallet).await - }, + Commands::Metadata { no_docs, stats_only, pallet } => + metadata::handle_metadata_command(node_url, no_docs, stats_only, pallet).await, Commands::Version => { log_print!("CLI Version: Quantus CLI v{}", env!("CARGO_PKG_VERSION")); Ok(()) diff --git a/src/cli/referenda.rs b/src/cli/referenda.rs index 9e2ead3..04f4416 100644 --- a/src/cli/referenda.rs +++ b/src/cli/referenda.rs @@ -178,7 +178,7 @@ pub async fn handle_referenda_command( let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; match command { - ReferendaCommands::SubmitRemark { message, from, password, password_file, origin } => { + ReferendaCommands::SubmitRemark { message, from, password, password_file, origin } => submit_remark_proposal( &quantus_client, &message, @@ -188,9 +188,8 @@ pub async fn handle_referenda_command( &origin, execution_mode, ) - .await - }, - ReferendaCommands::Submit { preimage_hash, from, password, password_file, origin } => { + .await, + ReferendaCommands::Submit { preimage_hash, from, password, password_file, origin } => submit_proposal( &quantus_client, &preimage_hash, @@ -200,14 +199,12 @@ pub async fn handle_referenda_command( &origin, execution_mode, ) - .await - }, + .await, ReferendaCommands::List => list_proposals(&quantus_client).await, - ReferendaCommands::Get { index, decode } => { - get_proposal_details(&quantus_client, index, decode).await - }, + ReferendaCommands::Get { index, decode } => + get_proposal_details(&quantus_client, index, decode).await, ReferendaCommands::Status { index } => get_proposal_status(&quantus_client, index).await, - ReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => { + ReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => place_decision_deposit( &quantus_client, index, @@ -216,8 +213,7 @@ pub async fn handle_referenda_command( password_file, execution_mode, ) - .await - }, + .await, ReferendaCommands::Vote { index, aye, @@ -226,7 +222,7 @@ pub async fn handle_referenda_command( from, password, password_file, - } => { + } => vote_on_referendum( &quantus_client, index, @@ -238,9 +234,8 @@ pub async fn handle_referenda_command( password_file, execution_mode, ) - .await - }, - ReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => { + .await, + ReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => refund_submission_deposit( &quantus_client, index, @@ -249,9 +244,8 @@ pub async fn handle_referenda_command( password_file, execution_mode, ) - .await - }, - ReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => { + .await, + ReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => refund_decision_deposit( &quantus_client, index, @@ -260,8 +254,7 @@ pub async fn handle_referenda_command( password_file, execution_mode, ) - .await - }, + .await, ReferendaCommands::Config => get_config(&quantus_client).await, } } @@ -349,12 +342,11 @@ async fn submit_remark_proposal( quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Root; quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) }, - _ => { + _ => return Err(QuantusError::Generic(format!( "Invalid origin type: {}. Must be 'signed', 'none', or 'root'", origin_type - ))) - }, + ))), }; let enactment = @@ -467,12 +459,11 @@ async fn submit_proposal( quantus_subxt::api::runtime_types::frame_support::dispatch::RawOrigin::Root; quantus_subxt::api::runtime_types::quantus_runtime::OriginCaller::system(raw_origin) }, - _ => { + _ => return Err(QuantusError::Generic(format!( "Invalid origin type: {}. Must be 'signed', 'none', or 'root'", origin_type - ))) - }, + ))), }; let enactment = @@ -733,8 +724,8 @@ async fn vote_on_referendum( let amount_value: u128 = (amount .parse::() .map_err(|_| QuantusError::Generic("Invalid amount format".to_string()))? - .max(0.0) - * 1_000_000_000_000_000_000.0) as u128; + .max(0.0) * + 1_000_000_000_000_000_000.0) as u128; // Validate conviction if conviction > 6 { diff --git a/src/cli/referenda_decode.rs b/src/cli/referenda_decode.rs index 7ee5aaf..7140fa7 100644 --- a/src/cli/referenda_decode.rs +++ b/src/cli/referenda_decode.rs @@ -22,9 +22,8 @@ pub async fn decode_preimage( let content = match preimage_result { Ok(Some(bounded_vec)) => bounded_vec.0, - Ok(None) => { - return Err(QuantusError::Generic(format!("Preimage not found for hash {:?}", hash))) - }, + Ok(None) => + return Err(QuantusError::Generic(format!("Preimage not found for hash {:?}", hash))), Err(e) => return Err(QuantusError::Generic(format!("Error fetching preimage: {:?}", e))), }; diff --git a/src/cli/reversible.rs b/src/cli/reversible.rs index 52bd779..63d5ed0 100644 --- a/src/cli/reversible.rs +++ b/src/cli/reversible.rs @@ -253,9 +253,8 @@ pub async fn handle_reversible_command( let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; match command { - ReversibleCommands::ListPending { address, from, password, password_file } => { - list_pending_transactions(&quantus_client, address, from, password, password_file).await - }, + ReversibleCommands::ListPending { address, from, password, password_file } => + list_pending_transactions(&quantus_client, address, from, password, password_file).await, ReversibleCommands::ScheduleTransfer { to, amount, from, password, password_file } => { // Parse and validate the amount let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; diff --git a/src/cli/scheduler.rs b/src/cli/scheduler.rs index 67ab6c2..e89a1cd 100644 --- a/src/cli/scheduler.rs +++ b/src/cli/scheduler.rs @@ -166,8 +166,7 @@ pub async fn handle_scheduler_command( Ok(()) }, SchedulerCommands::Agenda { range } => list_agenda_range(&quantus_client, &range).await, - SchedulerCommands::ScheduleRemark { after, from } => { - schedule_remark(&quantus_client, after, &from, execution_mode).await - }, + SchedulerCommands::ScheduleRemark { after, from } => + schedule_remark(&quantus_client, after, &from, execution_mode).await, } } diff --git a/src/cli/storage.rs b/src/cli/storage.rs index 3567190..2ad0ce0 100644 --- a/src/cli/storage.rs +++ b/src/cli/storage.rs @@ -839,18 +839,14 @@ pub async fn handle_storage_command( .await } }, - StorageCommands::List { pallet, names_only } => { - list_storage_items(&quantus_client, &pallet, names_only).await - }, - StorageCommands::ListPallets { with_counts } => { - list_pallets_with_storage(&quantus_client, with_counts).await - }, - StorageCommands::Stats { pallet, detailed } => { - show_storage_stats(&quantus_client, pallet, detailed).await - }, - StorageCommands::Iterate { pallet, name, limit, decode_as, block } => { - iterate_storage_entries(&quantus_client, &pallet, &name, limit, decode_as, block).await - }, + StorageCommands::List { pallet, names_only } => + list_storage_items(&quantus_client, &pallet, names_only).await, + StorageCommands::ListPallets { with_counts } => + list_pallets_with_storage(&quantus_client, with_counts).await, + StorageCommands::Stats { pallet, detailed } => + show_storage_stats(&quantus_client, pallet, detailed).await, + StorageCommands::Iterate { pallet, name, limit, decode_as, block } => + iterate_storage_entries(&quantus_client, &pallet, &name, limit, decode_as, block).await, StorageCommands::Set { pallet, name, value, wallet, password, password_file, r#type } => { log_print!("✍️ Setting storage for {}::{}", pallet.bright_green(), name.bright_cyan()); @@ -894,11 +890,10 @@ pub async fn handle_storage_command( .map_err(|e| QuantusError::Generic(format!("Invalid hex value: {e}")))? } }, - Some(unsupported) => { + Some(unsupported) => return Err(QuantusError::Generic(format!( "Unsupported type for --type: {unsupported}" - ))) - }, + ))), }; log_verbose!("Encoded value bytes: 0x{}", hex::encode(&value_bytes).dimmed()); diff --git a/src/cli/tech_collective.rs b/src/cli/tech_collective.rs index f917dd1..6234b6f 100644 --- a/src/cli/tech_collective.rs +++ b/src/cli/tech_collective.rs @@ -429,7 +429,7 @@ pub async fn handle_tech_collective_command( // Get actual member list match get_member_list(&quantus_client).await { - Ok(members) => { + Ok(members) => if members.is_empty() { log_print!("📭 No members in Tech Collective"); } else { @@ -443,8 +443,7 @@ pub async fn handle_tech_collective_command( member.to_quantus_ss58().bright_green() ); } - } - }, + }, Err(e) => { log_verbose!("⚠️ Failed to get member list: {:?}", e); // Fallback to member count diff --git a/src/cli/tech_referenda.rs b/src/cli/tech_referenda.rs index 4589c1b..6fc8860 100644 --- a/src/cli/tech_referenda.rs +++ b/src/cli/tech_referenda.rs @@ -192,7 +192,7 @@ pub async fn handle_tech_referenda_command( let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?; match command { - TechReferendaCommands::Submit { preimage_hash, from, password, password_file } => { + TechReferendaCommands::Submit { preimage_hash, from, password, password_file } => submit_runtime_upgrade( &quantus_client, &preimage_hash, @@ -201,9 +201,8 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await - }, - TechReferendaCommands::SubmitWithPreimage { wasm_file, from, password, password_file } => { + .await, + TechReferendaCommands::SubmitWithPreimage { wasm_file, from, password, password_file } => submit_runtime_upgrade_with_preimage( &quantus_client, &wasm_file, @@ -212,14 +211,12 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await - }, + .await, TechReferendaCommands::List => list_proposals(&quantus_client).await, TechReferendaCommands::Get { index } => get_proposal_details(&quantus_client, index).await, - TechReferendaCommands::Status { index } => { - get_proposal_status(&quantus_client, index).await - }, - TechReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => { + TechReferendaCommands::Status { index } => + get_proposal_status(&quantus_client, index).await, + TechReferendaCommands::PlaceDecisionDeposit { index, from, password, password_file } => place_decision_deposit( &quantus_client, index, @@ -228,21 +225,17 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await - }, - TechReferendaCommands::Cancel { index, from, password, password_file } => { + .await, + TechReferendaCommands::Cancel { index, from, password, password_file } => cancel_proposal(&quantus_client, index, &from, password, password_file, execution_mode) - .await - }, - TechReferendaCommands::Kill { index, from, password, password_file } => { + .await, + TechReferendaCommands::Kill { index, from, password, password_file } => kill_proposal(&quantus_client, index, &from, password, password_file, execution_mode) - .await - }, - TechReferendaCommands::Nudge { index, from, password, password_file } => { + .await, + TechReferendaCommands::Nudge { index, from, password, password_file } => nudge_proposal(&quantus_client, index, &from, password, password_file, execution_mode) - .await - }, - TechReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => { + .await, + TechReferendaCommands::RefundSubmissionDeposit { index, from, password, password_file } => refund_submission_deposit( &quantus_client, index, @@ -251,9 +244,8 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await - }, - TechReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => { + .await, + TechReferendaCommands::RefundDecisionDeposit { index, from, password, password_file } => refund_decision_deposit( &quantus_client, index, @@ -262,8 +254,7 @@ pub async fn handle_tech_referenda_command( password_file, execution_mode, ) - .await - }, + .await, TechReferendaCommands::Config => get_config(&quantus_client).await, } } diff --git a/src/cli/transfers.rs b/src/cli/transfers.rs index d4c3872..1926ed8 100644 --- a/src/cli/transfers.rs +++ b/src/cli/transfers.rs @@ -4,10 +4,12 @@ //! which allows clients to retrieve their transactions without revealing their //! exact addresses to the indexer. -use crate::error::{QuantusError, Result}; -use crate::subsquid::{compute_address_hash, get_hash_prefix, SubsquidClient, TransferQueryParams}; -use crate::wallet::WalletManager; -use crate::{log_error, log_print, log_success, log_verbose}; +use crate::{ + error::{QuantusError, Result}, + log_error, log_print, log_success, log_verbose, + subsquid::{compute_address_hash, get_hash_prefix, SubsquidClient, TransferQueryParams}, + wallet::WalletManager, +}; use clap::Subcommand; use colored::Colorize; use sp_core::crypto::{AccountId32, Ss58Codec}; @@ -75,7 +77,7 @@ pub async fn handle_transfers_command(cmd: TransfersCommands) -> Result<()> { limit, wallet, json, - } => { + } => handle_query_command( subsquid_url, prefix_len, @@ -86,11 +88,9 @@ pub async fn handle_transfers_command(cmd: TransfersCommands) -> Result<()> { wallet, json, ) - .await - }, - TransfersCommands::HashAddress { address, prefix_len } => { - handle_hash_address_command(&address, prefix_len) - }, + .await, + TransfersCommands::HashAddress { address, prefix_len } => + handle_hash_address_command(&address, prefix_len), } } diff --git a/src/cli/treasury.rs b/src/cli/treasury.rs index b82509a..e910bea 100644 --- a/src/cli/treasury.rs +++ b/src/cli/treasury.rs @@ -132,7 +132,7 @@ pub async fn handle_treasury_command( from, password, password_file, - } => { + } => submit_spend_referendum( &quantus_client, &beneficiary, @@ -143,13 +143,11 @@ pub async fn handle_treasury_command( password_file, execution_mode, ) - .await - }, - TreasuryCommands::Payout { index, from, password, password_file } => { + .await, + TreasuryCommands::Payout { index, from, password, password_file } => payout_spend(&quantus_client, index, &from, password, password_file, execution_mode) - .await - }, - TreasuryCommands::CheckStatus { index, from, password, password_file } => { + .await, + TreasuryCommands::CheckStatus { index, from, password, password_file } => check_spend_status( &quantus_client, index, @@ -158,10 +156,9 @@ pub async fn handle_treasury_command( password_file, execution_mode, ) - .await - }, + .await, TreasuryCommands::ListSpends => list_spends(&quantus_client).await, - TreasuryCommands::SpendSudo { beneficiary, amount, from, password, password_file } => { + TreasuryCommands::SpendSudo { beneficiary, amount, from, password, password_file } => spend_sudo( &quantus_client, &beneficiary, @@ -171,8 +168,7 @@ pub async fn handle_treasury_command( password_file, execution_mode, ) - .await - }, + .await, } } diff --git a/src/cli/wallet.rs b/src/cli/wallet.rs index b1425d5..ee30a21 100644 --- a/src/cli/wallet.rs +++ b/src/cli/wallet.rs @@ -224,7 +224,7 @@ pub async fn handle_wallet_command( if all { // Show all wallets (same as list command but with different header) match wallet_manager.list_wallets() { - Ok(wallets) => { + Ok(wallets) => if wallets.is_empty() { log_print!("{}", "No wallets found.".dimmed()); } else { @@ -254,8 +254,7 @@ pub async fn handle_wallet_command( log_print!(); } } - } - }, + }, Err(e) => { log_error!("{}", format!("❌ Failed to view wallets: {e}").red()); return Err(e); @@ -446,7 +445,7 @@ pub async fn handle_wallet_command( let wallet_manager = WalletManager::new()?; match wallet_manager.list_wallets() { - Ok(wallets) => { + Ok(wallets) => if wallets.is_empty() { log_print!("{}", "No wallets found.".dimmed()); log_print!( @@ -482,8 +481,7 @@ pub async fn handle_wallet_command( "💡 Use 'quantus wallet view --name ' to see full details" .dimmed() ); - } - }, + }, Err(e) => { log_error!("{}", format!("❌ Failed to list wallets: {e}").red()); return Err(e); From 5fbcced4c3ac67559bf1ebdd05e2fbd4f61ad642 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 26 Jan 2026 16:35:46 +0800 Subject: [PATCH 5/7] clippy --- src/subsquid/hash.rs | 8 -------- src/subsquid/types.rs | 2 ++ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/subsquid/hash.rs b/src/subsquid/hash.rs index e7ebb25..349687c 100644 --- a/src/subsquid/hash.rs +++ b/src/subsquid/hash.rs @@ -32,14 +32,6 @@ pub fn get_hash_prefix(hash: &str, prefix_len: usize) -> String { hash.chars().take(prefix_len).collect() } -/// Compute the hash prefix for a raw address with the specified prefix length. -/// -/// Convenience function combining `compute_address_hash` and `get_hash_prefix`. -pub fn compute_address_prefix(raw_address: &[u8; 32], prefix_len: usize) -> String { - let hash = compute_address_hash(raw_address); - get_hash_prefix(&hash, prefix_len) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/subsquid/types.rs b/src/subsquid/types.rs index 049219e..00aba1f 100644 --- a/src/subsquid/types.rs +++ b/src/subsquid/types.rs @@ -105,6 +105,7 @@ impl TransferQueryParams { self } + #[allow(dead_code)] pub fn with_offset(mut self, offset: u32) -> Self { self.offset = offset; self @@ -125,6 +126,7 @@ impl TransferQueryParams { self } + #[allow(dead_code)] pub fn with_max_amount(mut self, amount: u128) -> Self { self.max_amount = Some(amount); self From cf96d3f126a896112cf4a0a8ab730509adf4ef3b Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 26 Jan 2026 18:38:18 +0800 Subject: [PATCH 6/7] remove unused --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c7473f1..5fc0e82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,6 @@ qp-dilithium-crypto = { version = "0.2.0", features = ["serde"] } # HTTP client for Subsquid queries reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } blake3 = "1.8" -base64 = "0.22" # Blockchain and RPC client codec = { package = "parity-scale-codec", version = "3.7", features = [ From 4daf2f627d952f089fe3c5bc5edb3a99089df9a2 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 26 Jan 2026 19:00:11 +0800 Subject: [PATCH 7/7] remove duplicate --- Cargo.lock | 1 - src/cli/transfers.rs | 24 +++--------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 734f1b8..3ae10f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3794,7 +3794,6 @@ version = "0.3.1" dependencies = [ "aes-gcm", "argon2", - "base64", "blake3", "chrono", "clap", diff --git a/src/cli/transfers.rs b/src/cli/transfers.rs index 1926ed8..1275d4b 100644 --- a/src/cli/transfers.rs +++ b/src/cli/transfers.rs @@ -5,6 +5,7 @@ //! exact addresses to the indexer. use crate::{ + cli::send::format_balance, error::{QuantusError, Result}, log_error, log_print, log_success, log_verbose, subsquid::{compute_address_hash, get_hash_prefix, SubsquidClient, TransferQueryParams}, @@ -203,9 +204,9 @@ async fn handle_query_command( (false, false) => "???".dimmed(), // Shouldn't happen }; - // Parse and format amount + // Parse and format amount (12 decimals is standard for Substrate) let amount: u128 = transfer.amount.parse().unwrap_or(0); - let formatted_amount = format_planck_amount(amount); + let formatted_amount = format!("{} DEV", format_balance(amount, 12)); log_print!( " [{}] {} | Block {} | {} | {} -> {}", @@ -252,25 +253,6 @@ fn handle_hash_address_command(address: &str, prefix_len: usize) -> Result<()> { Ok(()) } -/// Format a planck amount to a human-readable string -fn format_planck_amount(planck: u128) -> String { - // Assuming 12 decimal places (standard for Substrate chains) - let decimals = 12u32; - let divisor = 10u128.pow(decimals); - let whole = planck / divisor; - let frac = planck % divisor; - - if frac == 0 { - format!("{} DEV", whole) - } else { - // Format with up to 4 decimal places - let frac_str = format!("{:012}", frac); - let trimmed = frac_str.trim_end_matches('0'); - let display_frac = if trimmed.len() > 4 { &trimmed[..4] } else { trimmed }; - format!("{}.{} DEV", whole, display_frac) - } -} - /// Truncate an address for display fn truncate_address(address: &str) -> String { if address.len() > 16 {