gemachain_cli_output/
display.rs

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
80// Pretty print a "name value"
81pub 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{}{}", // accounts are always readable...
138        if message.is_signer(index) {
139            "s" // stands for signer
140        } else {
141            "-"
142        },
143        if message.is_writable(index, /*demote_program_write_locks=*/ true) {
144            "w" // comment for consistent rust fmt (no joking; lol)
145        } else {
146            "-"
147        },
148        // account may be executable on-chain while not being
149        // designated as a program-id in the message
150        if message.maybe_executable(index) {
151            "x"
152        } else {
153            // programs to be executed via CPI cannot be identified as
154            // executable from the message
155            "-"
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
416/// Creates a new process bar for processing that will take an unknown amount of time
417pub 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}