1use crate::{
8 cli::send::format_balance,
9 error::{QuantusError, Result},
10 log_error, log_print, log_success, log_verbose,
11 subsquid::{compute_address_hash, get_hash_prefix, SubsquidClient, TransferQueryParams},
12 wallet::WalletManager,
13};
14use clap::Subcommand;
15use colored::Colorize;
16use sp_core::crypto::{AccountId32, Ss58Codec};
17
18#[derive(Subcommand, Debug)]
20pub enum TransfersCommands {
21 Query {
23 #[arg(long)]
25 subsquid_url: String,
26
27 #[arg(long, default_value = "4")]
31 prefix_len: usize,
32
33 #[arg(long)]
35 after_block: Option<u32>,
36
37 #[arg(long)]
39 before_block: Option<u32>,
40
41 #[arg(long)]
43 min_amount: Option<u128>,
44
45 #[arg(long, default_value = "100")]
47 limit: u32,
48
49 #[arg(long)]
51 wallet: Option<String>,
52
53 #[arg(long)]
55 json: bool,
56 },
57
58 HashAddress {
60 address: String,
62
63 #[arg(long, default_value = "4")]
65 prefix_len: usize,
66 },
67}
68
69pub async fn handle_transfers_command(cmd: TransfersCommands) -> Result<()> {
71 match cmd {
72 TransfersCommands::Query {
73 subsquid_url,
74 prefix_len,
75 after_block,
76 before_block,
77 min_amount,
78 limit,
79 wallet,
80 json,
81 } =>
82 handle_query_command(
83 subsquid_url,
84 prefix_len,
85 after_block,
86 before_block,
87 min_amount,
88 limit,
89 wallet,
90 json,
91 )
92 .await,
93 TransfersCommands::HashAddress { address, prefix_len } =>
94 handle_hash_address_command(&address, prefix_len),
95 }
96}
97
98#[allow(clippy::too_many_arguments)]
100async fn handle_query_command(
101 subsquid_url: String,
102 prefix_len: usize,
103 after_block: Option<u32>,
104 before_block: Option<u32>,
105 min_amount: Option<u128>,
106 limit: u32,
107 wallet_name: Option<String>,
108 json_output: bool,
109) -> Result<()> {
110 if prefix_len == 0 || prefix_len > 64 {
112 return Err(QuantusError::Generic("Prefix length must be between 1 and 64".to_string()));
113 }
114
115 let wallet_manager = WalletManager::new()?;
117 let wallets = wallet_manager.list_wallets()?;
118
119 if wallets.is_empty() {
120 log_error!("No wallets found. Create a wallet first with 'quantus wallet create'");
121 return Ok(());
122 }
123
124 let wallets_to_query: Vec<_> = if let Some(name) = &wallet_name {
126 wallets.into_iter().filter(|w| w.name == *name).collect()
127 } else {
128 wallets
129 };
130
131 if wallets_to_query.is_empty() {
132 log_error!("No matching wallet found");
133 return Ok(());
134 }
135
136 let mut raw_addresses: Vec<[u8; 32]> = Vec::new();
138 for wallet in &wallets_to_query {
139 let account_id = AccountId32::from_ss58check(&wallet.address).map_err(|e| {
140 QuantusError::Generic(format!("Invalid address {}: {}", wallet.address, e))
141 })?;
142 raw_addresses.push(account_id.into());
143 }
144
145 if !json_output {
146 log_print!("{}", "Privacy-Preserving Transfer Query".bright_cyan().bold());
147 log_print!("");
148 log_print!(
149 " Querying for {} wallet(s) with prefix length {}",
150 wallets_to_query.len().to_string().bright_yellow(),
151 prefix_len.to_string().bright_yellow()
152 );
153 log_print!(
154 " Privacy level: ~1/{} of address space per query",
155 (1u64 << (prefix_len * 4)).to_string().bright_green()
156 );
157 log_print!("");
158 }
159
160 let client = SubsquidClient::new(subsquid_url)?;
162
163 let mut params = TransferQueryParams::new().with_limit(limit);
165 if let Some(block) = after_block {
166 params = params.with_after_block(block);
167 }
168 if let Some(block) = before_block {
169 params = params.with_before_block(block);
170 }
171 if let Some(amount) = min_amount {
172 params = params.with_min_amount(amount);
173 }
174
175 let transfers =
177 client.query_transfers_for_addresses(&raw_addresses, prefix_len, params).await?;
178
179 if json_output {
180 let json = serde_json::to_string_pretty(&transfers)
182 .map_err(|e| QuantusError::Generic(format!("Failed to serialize transfers: {}", e)))?;
183 println!("{}", json);
184 } else {
185 if transfers.is_empty() {
187 log_print!("No transfers found for your addresses.");
188 } else {
189 log_success!("Found {} transfers:", transfers.len().to_string().bright_green());
190 log_print!("");
191
192 for transfer in &transfers {
193 let our_address_hashes: std::collections::HashSet<String> =
195 raw_addresses.iter().map(compute_address_hash).collect();
196
197 let is_incoming = our_address_hashes.contains(&transfer.to_hash);
198 let is_outgoing = our_address_hashes.contains(&transfer.from_hash);
199
200 let direction = match (is_incoming, is_outgoing) {
201 (true, true) => "SELF".bright_blue(),
202 (true, false) => "IN".bright_green(),
203 (false, true) => "OUT".bright_red(),
204 (false, false) => "???".dimmed(), };
206
207 let amount: u128 = transfer.amount.parse().unwrap_or(0);
209 let formatted_amount = format!("{} DEV", format_balance(amount, 12));
210
211 log_print!(
212 " [{}] {} | Block {} | {} | {} -> {}",
213 direction,
214 &transfer.timestamp[..19], transfer.block_height.to_string().bright_yellow(),
216 formatted_amount.bright_cyan(),
217 truncate_address(&transfer.from_id),
218 truncate_address(&transfer.to_id),
219 );
220
221 if let Some(hash) = &transfer.extrinsic_hash {
222 log_verbose!(" Extrinsic: {}", hash.dimmed());
223 }
224 }
225 }
226 }
227
228 Ok(())
229}
230
231fn handle_hash_address_command(address: &str, prefix_len: usize) -> Result<()> {
233 let account_id = AccountId32::from_ss58check(address)
235 .map_err(|e| QuantusError::Generic(format!("Invalid address: {}", e)))?;
236
237 let raw_address: [u8; 32] = account_id.into();
238 let full_hash = compute_address_hash(&raw_address);
239 let prefix = get_hash_prefix(&full_hash, prefix_len);
240
241 log_print!("{}", "Address Hash Information".bright_cyan().bold());
242 log_print!("");
243 log_print!(" Address: {}", address.bright_yellow());
244 log_print!(" Full Hash: {}", full_hash.dimmed());
245 log_print!(" Prefix ({}): {}", prefix_len, prefix.bright_green().bold());
246 log_print!("");
247 log_print!(
248 " Privacy: With prefix length {}, your query will match ~1/{} of all addresses",
249 prefix_len,
250 (1u64 << (prefix_len * 4)).to_string().bright_cyan()
251 );
252
253 Ok(())
254}
255
256fn truncate_address(address: &str) -> String {
258 if address.len() > 16 {
259 format!("{}...{}", &address[..8], &address[address.len() - 6..])
260 } else {
261 address.to_string()
262 }
263}