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},
wallet::WalletManager,
};
use clap::Subcommand;
use colored::Colorize;
use sp_core::crypto::{AccountId32, Ss58Codec};
#[derive(Subcommand, Debug)]
pub enum TransfersCommands {
Query {
#[arg(long, default_value = "https://subsquid.quantus.com/blue/graphql")]
subsquid_url: String,
#[arg(long, default_value = "4")]
prefix_len: usize,
#[arg(long)]
after_block: Option<u32>,
#[arg(long)]
before_block: Option<u32>,
#[arg(long)]
min_amount: Option<u128>,
#[arg(long, default_value = "100")]
limit: u32,
#[arg(long)]
wallet: Option<String>,
#[arg(long)]
json: bool,
},
HashAddress {
address: String,
#[arg(long, default_value = "4")]
prefix_len: usize,
},
}
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),
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_query_command(
subsquid_url: String,
prefix_len: usize,
after_block: Option<u32>,
before_block: Option<u32>,
min_amount: Option<u128>,
limit: u32,
wallet_name: Option<String>,
json_output: bool,
) -> Result<()> {
if prefix_len == 0 || prefix_len > 64 {
return Err(QuantusError::Generic("Prefix length must be between 1 and 64".to_string()));
}
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(());
}
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(());
}
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!("");
}
let client = SubsquidClient::new(subsquid_url)?;
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);
}
let transfers =
client.query_transfers_for_addresses(&raw_addresses, prefix_len, params).await?;
if json_output {
let json = serde_json::to_string_pretty(&transfers)
.map_err(|e| QuantusError::Generic(format!("Failed to serialize transfers: {}", e)))?;
println!("{}", json);
} else {
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 {
let our_address_hashes: std::collections::HashSet<String> =
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(), };
let amount: u128 = transfer.amount.parse().unwrap_or(0);
let formatted_amount = format!("{} DEV", format_balance(amount, 12));
log_print!(
" [{}] {} | Block {} | {} | {} -> {}",
direction,
&transfer.timestamp[..19], 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(())
}
fn handle_hash_address_command(address: &str, prefix_len: usize) -> Result<()> {
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(())
}
fn truncate_address(address: &str) -> String {
if address.len() > 16 {
format!("{}...{}", &address[..8], &address[address.len() - 6..])
} else {
address.to_string()
}
}