trident_explorer/
output.rs

1use crate::{
2    account::{AccountFieldVisibility, DisplayKeyedAccount, KeyedAccount},
3    config::ExplorerConfig,
4    display::DisplayFormat,
5    error::{ExplorerError, Result},
6    program::{DisplayUpgradeableProgram, ProgramFieldVisibility},
7    transaction::{
8        DisplayRawTransaction, DisplayTransaction, RawTransactionFieldVisibility,
9        TransactionFieldVisibility,
10    },
11};
12use console::style;
13use pretty_hex::*;
14use solana_client::rpc_config::RpcTransactionConfig;
15use solana_sdk::{
16    account_utils::StateMut, bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable,
17    bpf_loader_upgradeable::UpgradeableLoaderState, commitment_config::CommitmentConfig,
18    native_token, pubkey::Pubkey, signature::Signature,
19};
20use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding};
21use std::{cmp::Ordering, fmt::Write};
22
23pub fn pretty_lamports_to_sol(lamports: u64) -> String {
24    let sol_str = format!("{:.9}", native_token::lamports_to_sol(lamports));
25    sol_str
26        .trim_end_matches('0')
27        .trim_end_matches('.')
28        .to_string()
29}
30
31pub fn classify_account(fee_payer: bool, writable: bool, signer: bool, program: bool) -> String {
32    let mut account_type_string = String::new();
33    let mut started = false;
34    if fee_payer {
35        account_type_string.push_str("[Fee Payer]");
36        started = true;
37    }
38    if writable {
39        if started {
40            account_type_string.push(' ');
41        }
42        account_type_string.push_str("[Writable]");
43        started = true;
44    }
45    if signer {
46        if started {
47            account_type_string.push(' ');
48        }
49        account_type_string.push_str("[Signer]");
50        started = true;
51    }
52    if program {
53        if started {
54            account_type_string.push(' ');
55        }
56        account_type_string.push_str("[Program]");
57    }
58    account_type_string
59}
60
61pub fn calculate_change(post: u64, pre: u64) -> String {
62    match post.cmp(&pre) {
63        Ordering::Greater => format!(
64            "◎ {} (+{})",
65            pretty_lamports_to_sol(post),
66            pretty_lamports_to_sol(post - pre)
67        ),
68        Ordering::Less => format!(
69            "◎ {} (-{})",
70            pretty_lamports_to_sol(post),
71            pretty_lamports_to_sol(pre - post)
72        ),
73        Ordering::Equal => format!("◎ {}", pretty_lamports_to_sol(post)),
74    }
75}
76
77pub fn change_in_sol(post: u64, pre: u64) -> String {
78    match post.cmp(&pre) {
79        Ordering::Greater => format!("+{}", pretty_lamports_to_sol(post - pre)),
80        Ordering::Less => format!("-{}", pretty_lamports_to_sol(pre - post)),
81        Ordering::Equal => "0".to_string(),
82    }
83}
84
85pub fn status_to_string(status: &TransactionConfirmationStatus) -> String {
86    match status {
87        TransactionConfirmationStatus::Processed => "Processed".to_string(),
88        TransactionConfirmationStatus::Confirmed => "Confirmed".to_string(),
89        TransactionConfirmationStatus::Finalized => "Finalized".to_string(),
90    }
91}
92
93pub async fn print_account(
94    pubkey: &Pubkey,
95    visibility: &AccountFieldVisibility,
96    format: DisplayFormat,
97    config: &ExplorerConfig,
98) -> Result<()> {
99    let account_string = get_account_string(pubkey, visibility, format, config).await?;
100    println!("{account_string}");
101    Ok(())
102}
103
104pub async fn print_program(
105    program_id: &Pubkey,
106    visibility: &ProgramFieldVisibility,
107    format: DisplayFormat,
108    config: &ExplorerConfig,
109) -> Result<()> {
110    let program_string = get_program_string(program_id, visibility, format, config).await?;
111    println!("{program_string}");
112    Ok(())
113}
114
115pub async fn print_raw_transaction(
116    signature: &Signature,
117    visibility: &RawTransactionFieldVisibility,
118    format: DisplayFormat,
119    config: &ExplorerConfig,
120) -> Result<()> {
121    let raw_transaction_string =
122        get_raw_transaction_string(signature, visibility, format, config).await?;
123    println!("{raw_transaction_string}");
124    Ok(())
125}
126
127pub async fn print_transaction(
128    signature: &Signature,
129    visibility: &TransactionFieldVisibility,
130    format: DisplayFormat,
131    config: &ExplorerConfig,
132) -> Result<()> {
133    let transaction_string = get_transaction_string(signature, visibility, format, config).await?;
134    println!("{transaction_string}");
135    Ok(())
136}
137
138pub async fn get_account_string(
139    pubkey: &Pubkey,
140    visibility: &AccountFieldVisibility,
141    format: DisplayFormat,
142    config: &ExplorerConfig,
143) -> Result<String> {
144    let rpc_client = config.rpc_client();
145    let account = rpc_client.get_account(pubkey)?;
146    let keyed_account = KeyedAccount {
147        pubkey: *pubkey,
148        account,
149    };
150    let display_keyed_account = DisplayKeyedAccount::from_keyed_account(&keyed_account, visibility);
151    let mut account_string = format.formatted_string(&display_keyed_account)?;
152
153    if display_keyed_account.account.data.is_some() {
154        let data = &keyed_account.account.data;
155        if let DisplayFormat::Cli = format {
156            if !data.is_empty() {
157                writeln!(&mut account_string)?;
158                writeln!(&mut account_string)?;
159
160                writeln!(
161                    &mut account_string,
162                    "{} {} bytes",
163                    style("Hexdump:").bold(),
164                    data.len()
165                )?;
166                // Show hexdump of not more than MAX_BYTES_SHOWN bytes
167                const MAX_BYTES_SHOWN: usize = 64;
168                let len = data.len();
169                let (end, finished) = if MAX_BYTES_SHOWN > len {
170                    (len, true)
171                } else {
172                    (MAX_BYTES_SHOWN, false)
173                };
174                let raw_account_data = &data[..end];
175                let cfg = HexConfig {
176                    title: false,
177                    width: 16,
178                    group: 0,
179                    chunk: 2,
180                    ..HexConfig::default()
181                };
182                write!(&mut account_string, "{:?}", raw_account_data.hex_conf(cfg))?;
183                if !finished {
184                    writeln!(&mut account_string)?;
185                    write!(&mut account_string, "... (skipped)")?;
186                }
187            }
188        };
189    }
190
191    Ok(account_string)
192}
193
194pub async fn get_program_string(
195    program_id: &Pubkey,
196    visibility: &ProgramFieldVisibility,
197    format: DisplayFormat,
198    config: &ExplorerConfig,
199) -> Result<String> {
200    let rpc_client = config.rpc_client();
201    let program_account = rpc_client.get_account(program_id)?;
202    let program_keyed_account = KeyedAccount {
203        pubkey: *program_id,
204        account: program_account,
205    };
206
207    if program_keyed_account.account.owner == bpf_loader::id()
208        || program_keyed_account.account.owner == bpf_loader_deprecated::id()
209    {
210        // these loaders are not interesting, just accounts with the program.so in data
211        let mut program_string = get_account_string(
212            program_id,
213            &AccountFieldVisibility::new_all_enabled(),
214            format,
215            config,
216        )
217        .await?;
218
219        if let DisplayFormat::Cli = format {
220            program_string.push_str(
221                "\n\nNote: the program is loaded either by the deprecated BPFLoader or BPFLoader2,
222it is an executable account with program.so in its data, hence this output.",
223            );
224        }
225
226        Ok(program_string)
227    } else if program_keyed_account.account.owner == bpf_loader_upgradeable::id() {
228        // this is the only interesting loader which uses redirection to programdata account
229        if let Ok(UpgradeableLoaderState::Program {
230            programdata_address,
231        }) = program_keyed_account.account.state()
232        {
233            if let Ok(programdata_account) = rpc_client.get_account(&programdata_address) {
234                let programdata_keyed_account = KeyedAccount {
235                    pubkey: programdata_address,
236                    account: programdata_account,
237                };
238                if let Ok(UpgradeableLoaderState::ProgramData {
239                    upgrade_authority_address,
240                    slot,
241                }) = programdata_keyed_account.account.state()
242                {
243                    let program = DisplayUpgradeableProgram::from(
244                        &program_keyed_account,
245                        &programdata_keyed_account,
246                        slot,
247                        &upgrade_authority_address,
248                        visibility,
249                    );
250                    let mut program_string = format.formatted_string(&program)?;
251
252                    if program.programdata_account.is_some() {
253                        if let DisplayFormat::Cli = format {
254                            writeln!(&mut program_string)?;
255                            writeln!(&mut program_string)?;
256                            writeln!(
257                                &mut program_string,
258                                "{} {} bytes",
259                                style("Followed by Raw Program Data (program.so):").bold(),
260                                programdata_keyed_account.account.data.len()
261                                    - UpgradeableLoaderState::size_of_programdata_metadata()
262                            )?;
263
264                            // Show hexdump of not more than MAX_BYTES_SHOWN bytes
265                            const MAX_BYTES_SHOWN: usize = 64;
266                            let len = programdata_keyed_account.account.data.len();
267                            let offset = UpgradeableLoaderState::size_of_programdata_metadata();
268                            let (end, finished) = if offset + MAX_BYTES_SHOWN > len {
269                                (len, true)
270                            } else {
271                                (offset + MAX_BYTES_SHOWN, false)
272                            };
273                            let raw_program_data =
274                                &programdata_keyed_account.account.data[offset..end];
275                            let cfg = HexConfig {
276                                title: false,
277                                width: 16,
278                                group: 0,
279                                chunk: 2,
280                                ..HexConfig::default()
281                            };
282                            write!(&mut program_string, "{:?}", raw_program_data.hex_conf(cfg))?;
283                            if !finished {
284                                writeln!(&mut program_string)?;
285                                write!(&mut program_string, "... (skipped)")?;
286                            }
287                        }
288                    }
289
290                    Ok(program_string)
291                } else {
292                    Err(ExplorerError::Custom(format!(
293                        "Program {program_id} has been closed"
294                    )))
295                }
296            } else {
297                Err(ExplorerError::Custom(format!(
298                    "Program {program_id} has been closed"
299                )))
300            }
301        } else {
302            Err(ExplorerError::Custom(format!(
303                "{program_id} is not a Program account"
304            )))
305        }
306    } else {
307        Err(ExplorerError::Custom(format!(
308            "{program_id} is not a pubkey of an on-chain BPF program."
309        )))
310    }
311}
312
313pub async fn get_raw_transaction_string(
314    signature: &Signature,
315    visibility: &RawTransactionFieldVisibility,
316    format: DisplayFormat,
317    config: &ExplorerConfig,
318) -> Result<String> {
319    let rpc_client = config.rpc_client();
320    let config = RpcTransactionConfig {
321        encoding: Some(UiTransactionEncoding::Binary),
322        commitment: Some(CommitmentConfig::confirmed()),
323        max_supported_transaction_version: None,
324    };
325
326    let transaction = rpc_client.get_transaction_with_config(signature, config)?;
327
328    let response = rpc_client.get_signature_statuses_with_history(&[*signature])?;
329
330    let transaction_status = response.value[0].as_ref().unwrap();
331
332    let display_transaction =
333        DisplayRawTransaction::from(&transaction, transaction_status, visibility)?;
334
335    let transaction_string = format.formatted_string(&display_transaction)?;
336
337    Ok(transaction_string)
338}
339
340pub async fn get_transaction_string(
341    signature: &Signature,
342    visibility: &TransactionFieldVisibility,
343    format: DisplayFormat,
344    config: &ExplorerConfig,
345) -> Result<String> {
346    let rpc_client = config.rpc_client();
347    let config = RpcTransactionConfig {
348        encoding: Some(UiTransactionEncoding::Binary),
349        commitment: Some(CommitmentConfig::confirmed()),
350        max_supported_transaction_version: Some(0),
351    };
352
353    let transaction = rpc_client.get_transaction_with_config(signature, config)?;
354
355    let response = rpc_client.get_signature_statuses_with_history(&[*signature])?;
356
357    let transaction_status = response.value[0].as_ref().unwrap();
358
359    let display_transaction =
360        DisplayTransaction::from(&transaction, transaction_status, visibility)?;
361
362    let transaction_string = format.formatted_string(&display_transaction)?;
363
364    Ok(transaction_string)
365}