1use {
2 crate::cli_output::CliSignatureVerificationStatus,
3 chrono::{DateTime, Local, NaiveDateTime, SecondsFormat, TimeZone, Utc},
4 console::style,
5 indicatif::{ProgressBar, ProgressStyle},
6 gemachain_sdk::{
7 clock::UnixTimestamp, hash::Hash, message::Message, native_token::carats_to_gema,
8 program_utils::limited_deserialize, pubkey::Pubkey, stake, transaction::Transaction,
9 },
10 gemachain_transaction_status::UiTransactionStatusMeta,
11 spl_memo::id as spl_memo_id,
12 spl_memo::v1::id as spl_memo_v1_id,
13 std::{collections::HashMap, fmt, io},
14};
15
16#[derive(Clone, Debug)]
17pub struct BuildBalanceMessageConfig {
18 pub use_carats_unit: bool,
19 pub show_unit: bool,
20 pub trim_trailing_zeros: bool,
21}
22
23impl Default for BuildBalanceMessageConfig {
24 fn default() -> Self {
25 Self {
26 use_carats_unit: false,
27 show_unit: true,
28 trim_trailing_zeros: true,
29 }
30 }
31}
32
33fn is_memo_program(k: &Pubkey) -> bool {
34 let k_str = k.to_string();
35 (k_str == spl_memo_v1_id().to_string()) || (k_str == spl_memo_id().to_string())
36}
37
38pub fn build_balance_message_with_config(
39 carats: u64,
40 config: &BuildBalanceMessageConfig,
41) -> String {
42 let value = if config.use_carats_unit {
43 carats.to_string()
44 } else {
45 let gema = carats_to_gema(carats);
46 let gema_str = format!("{:.9}", gema);
47 if config.trim_trailing_zeros {
48 gema_str
49 .trim_end_matches('0')
50 .trim_end_matches('.')
51 .to_string()
52 } else {
53 gema_str
54 }
55 };
56 let unit = if config.show_unit {
57 if config.use_carats_unit {
58 let ess = if carats == 1 { "" } else { "s" };
59 format!(" carat{}", ess)
60 } else {
61 " GEMA".to_string()
62 }
63 } else {
64 "".to_string()
65 };
66 format!("{}{}", value, unit)
67}
68
69pub fn build_balance_message(carats: u64, use_carats_unit: bool, show_unit: bool) -> String {
70 build_balance_message_with_config(
71 carats,
72 &BuildBalanceMessageConfig {
73 use_carats_unit,
74 show_unit,
75 ..BuildBalanceMessageConfig::default()
76 },
77 )
78}
79
80pub fn println_name_value(name: &str, value: &str) {
82 let styled_value = if value.is_empty() {
83 style("(not set)").italic()
84 } else {
85 style(value)
86 };
87 println!("{} {}", style(name).bold(), styled_value);
88}
89
90pub fn writeln_name_value(f: &mut dyn fmt::Write, name: &str, value: &str) -> fmt::Result {
91 let styled_value = if value.is_empty() {
92 style("(not set)").italic()
93 } else {
94 style(value)
95 };
96 writeln!(f, "{} {}", style(name).bold(), styled_value)
97}
98
99pub fn format_labeled_address(pubkey: &str, address_labels: &HashMap<String, String>) -> String {
100 let label = address_labels.get(pubkey);
101 match label {
102 Some(label) => format!(
103 "{:.31} ({:.4}..{})",
104 label,
105 pubkey,
106 pubkey.split_at(pubkey.len() - 4).1
107 ),
108 None => pubkey.to_string(),
109 }
110}
111
112pub fn println_signers(
113 blockhash: &Hash,
114 signers: &[String],
115 absent: &[String],
116 bad_sig: &[String],
117) {
118 println!();
119 println!("Blockhash: {}", blockhash);
120 if !signers.is_empty() {
121 println!("Signers (Pubkey=Signature):");
122 signers.iter().for_each(|signer| println!(" {}", signer))
123 }
124 if !absent.is_empty() {
125 println!("Absent Signers (Pubkey):");
126 absent.iter().for_each(|pubkey| println!(" {}", pubkey))
127 }
128 if !bad_sig.is_empty() {
129 println!("Bad Signatures (Pubkey):");
130 bad_sig.iter().for_each(|pubkey| println!(" {}", pubkey))
131 }
132 println!();
133}
134
135fn format_account_mode(message: &Message, index: usize) -> String {
136 format!(
137 "{}r{}{}", if message.is_signer(index) {
139 "s" } else {
141 "-"
142 },
143 if message.is_writable(index, true) {
144 "w" } else {
146 "-"
147 },
148 if message.maybe_executable(index) {
151 "x"
152 } else {
153 "-"
156 },
157 )
158}
159
160pub fn write_transaction<W: io::Write>(
161 w: &mut W,
162 transaction: &Transaction,
163 transaction_status: &Option<UiTransactionStatusMeta>,
164 prefix: &str,
165 sigverify_status: Option<&[CliSignatureVerificationStatus]>,
166 block_time: Option<UnixTimestamp>,
167) -> io::Result<()> {
168 let message = &transaction.message;
169 if let Some(block_time) = block_time {
170 writeln!(
171 w,
172 "{}Block Time: {:?}",
173 prefix,
174 Local.timestamp(block_time, 0)
175 )?;
176 }
177 writeln!(
178 w,
179 "{}Recent Blockhash: {:?}",
180 prefix, message.recent_blockhash
181 )?;
182 let sigverify_statuses = if let Some(sigverify_status) = sigverify_status {
183 sigverify_status
184 .iter()
185 .map(|s| format!(" ({})", s))
186 .collect()
187 } else {
188 vec!["".to_string(); transaction.signatures.len()]
189 };
190 for (signature_index, (signature, sigverify_status)) in transaction
191 .signatures
192 .iter()
193 .zip(&sigverify_statuses)
194 .enumerate()
195 {
196 writeln!(
197 w,
198 "{}Signature {}: {:?}{}",
199 prefix, signature_index, signature, sigverify_status,
200 )?;
201 }
202 let mut fee_payer_index = None;
203 for (account_index, account) in message.account_keys.iter().enumerate() {
204 if fee_payer_index.is_none() && message.is_non_loader_key(account_index) {
205 fee_payer_index = Some(account_index)
206 }
207 writeln!(
208 w,
209 "{}Account {}: {} {}{}",
210 prefix,
211 account_index,
212 format_account_mode(message, account_index),
213 account,
214 if Some(account_index) == fee_payer_index {
215 " (fee payer)"
216 } else {
217 ""
218 },
219 )?;
220 }
221 for (instruction_index, instruction) in message.instructions.iter().enumerate() {
222 let program_pubkey = message.account_keys[instruction.program_id_index as usize];
223 writeln!(w, "{}Instruction {}", prefix, instruction_index)?;
224 writeln!(
225 w,
226 "{} Program: {} ({})",
227 prefix, program_pubkey, instruction.program_id_index
228 )?;
229 for (account_index, account) in instruction.accounts.iter().enumerate() {
230 let account_pubkey = message.account_keys[*account as usize];
231 writeln!(
232 w,
233 "{} Account {}: {} ({})",
234 prefix, account_index, account_pubkey, account
235 )?;
236 }
237
238 let mut raw = true;
239 if program_pubkey == gemachain_vote_program::id() {
240 if let Ok(vote_instruction) = limited_deserialize::<
241 gemachain_vote_program::vote_instruction::VoteInstruction,
242 >(&instruction.data)
243 {
244 writeln!(w, "{} {:?}", prefix, vote_instruction)?;
245 raw = false;
246 }
247 } else if program_pubkey == stake::program::id() {
248 if let Ok(stake_instruction) =
249 limited_deserialize::<stake::instruction::StakeInstruction>(&instruction.data)
250 {
251 writeln!(w, "{} {:?}", prefix, stake_instruction)?;
252 raw = false;
253 }
254 } else if program_pubkey == gemachain_sdk::system_program::id() {
255 if let Ok(system_instruction) = limited_deserialize::<
256 gemachain_sdk::system_instruction::SystemInstruction,
257 >(&instruction.data)
258 {
259 writeln!(w, "{} {:?}", prefix, system_instruction)?;
260 raw = false;
261 }
262 } else if is_memo_program(&program_pubkey) {
263 if let Ok(s) = std::str::from_utf8(&instruction.data) {
264 writeln!(w, "{} Data: \"{}\"", prefix, s)?;
265 raw = false;
266 }
267 }
268
269 if raw {
270 writeln!(w, "{} Data: {:?}", prefix, instruction.data)?;
271 }
272 }
273
274 if let Some(transaction_status) = transaction_status {
275 writeln!(
276 w,
277 "{}Status: {}",
278 prefix,
279 match &transaction_status.status {
280 Ok(_) => "Ok".into(),
281 Err(err) => err.to_string(),
282 }
283 )?;
284 writeln!(
285 w,
286 "{} Fee: GM{}",
287 prefix,
288 carats_to_gema(transaction_status.fee)
289 )?;
290 assert_eq!(
291 transaction_status.pre_balances.len(),
292 transaction_status.post_balances.len()
293 );
294 for (i, (pre, post)) in transaction_status
295 .pre_balances
296 .iter()
297 .zip(transaction_status.post_balances.iter())
298 .enumerate()
299 {
300 if pre == post {
301 writeln!(
302 w,
303 "{} Account {} balance: GM{}",
304 prefix,
305 i,
306 carats_to_gema(*pre)
307 )?;
308 } else {
309 writeln!(
310 w,
311 "{} Account {} balance: GM{} -> GM{}",
312 prefix,
313 i,
314 carats_to_gema(*pre),
315 carats_to_gema(*post)
316 )?;
317 }
318 }
319
320 if let Some(log_messages) = &transaction_status.log_messages {
321 if !log_messages.is_empty() {
322 writeln!(w, "{}Log Messages:", prefix,)?;
323 for log_message in log_messages {
324 writeln!(w, "{} {}", prefix, log_message)?;
325 }
326 }
327 }
328
329 if let Some(rewards) = &transaction_status.rewards {
330 if !rewards.is_empty() {
331 writeln!(w, "{}Rewards:", prefix,)?;
332 writeln!(
333 w,
334 "{} {:<44} {:^15} {:<15} {:<20}",
335 prefix, "Address", "Type", "Amount", "New Balance"
336 )?;
337 for reward in rewards {
338 let sign = if reward.carats < 0 { "-" } else { "" };
339 writeln!(
340 w,
341 "{} {:<44} {:^15} {:<15} {}",
342 prefix,
343 reward.pubkey,
344 if let Some(reward_type) = reward.reward_type {
345 format!("{}", reward_type)
346 } else {
347 "-".to_string()
348 },
349 format!(
350 "{}GM{:<14.9}",
351 sign,
352 carats_to_gema(reward.carats.abs() as u64)
353 ),
354 format!("GM{:<18.9}", carats_to_gema(reward.post_balance),)
355 )?;
356 }
357 }
358 }
359 } else {
360 writeln!(w, "{}Status: Unavailable", prefix)?;
361 }
362
363 Ok(())
364}
365
366pub fn println_transaction(
367 transaction: &Transaction,
368 transaction_status: &Option<UiTransactionStatusMeta>,
369 prefix: &str,
370 sigverify_status: Option<&[CliSignatureVerificationStatus]>,
371 block_time: Option<UnixTimestamp>,
372) {
373 let mut w = Vec::new();
374 if write_transaction(
375 &mut w,
376 transaction,
377 transaction_status,
378 prefix,
379 sigverify_status,
380 block_time,
381 )
382 .is_ok()
383 {
384 if let Ok(s) = String::from_utf8(w) {
385 print!("{}", s);
386 }
387 }
388}
389
390pub fn writeln_transaction(
391 f: &mut dyn fmt::Write,
392 transaction: &Transaction,
393 transaction_status: &Option<UiTransactionStatusMeta>,
394 prefix: &str,
395 sigverify_status: Option<&[CliSignatureVerificationStatus]>,
396 block_time: Option<UnixTimestamp>,
397) -> fmt::Result {
398 let mut w = Vec::new();
399 if write_transaction(
400 &mut w,
401 transaction,
402 transaction_status,
403 prefix,
404 sigverify_status,
405 block_time,
406 )
407 .is_ok()
408 {
409 if let Ok(s) = String::from_utf8(w) {
410 write!(f, "{}", s)?;
411 }
412 }
413 Ok(())
414}
415
416pub fn new_spinner_progress_bar() -> ProgressBar {
418 let progress_bar = ProgressBar::new(42);
419 progress_bar
420 .set_style(ProgressStyle::default_spinner().template("{spinner:.green} {wide_msg}"));
421 progress_bar.enable_steady_tick(100);
422 progress_bar
423}
424
425pub fn unix_timestamp_to_string(unix_timestamp: UnixTimestamp) -> String {
426 match NaiveDateTime::from_timestamp_opt(unix_timestamp, 0) {
427 Some(ndt) => DateTime::<Utc>::from_utc(ndt, Utc).to_rfc3339_opts(SecondsFormat::Secs, true),
428 None => format!("UnixTimestamp {}", unix_timestamp),
429 }
430}
431
432#[cfg(test)]
433mod test {
434 use super::*;
435 use gemachain_sdk::pubkey::Pubkey;
436
437 #[test]
438 fn test_format_labeled_address() {
439 let pubkey = Pubkey::default().to_string();
440 let mut address_labels = HashMap::new();
441
442 assert_eq!(format_labeled_address(&pubkey, &address_labels), pubkey);
443
444 address_labels.insert(pubkey.to_string(), "Default Address".to_string());
445 assert_eq!(
446 &format_labeled_address(&pubkey, &address_labels),
447 "Default Address (1111..1111)"
448 );
449
450 address_labels.insert(
451 pubkey.to_string(),
452 "abcdefghijklmnopqrstuvwxyz1234567890".to_string(),
453 );
454 assert_eq!(
455 &format_labeled_address(&pubkey, &address_labels),
456 "abcdefghijklmnopqrstuvwxyz12345 (1111..1111)"
457 );
458 }
459}