Skip to main content

solana_tools_lite_cli/flows/presenter/
analysis_presenter.rs

1//! Presentation rules for transaction signing summaries.
2
3use crate::flows::presenter::{emit_line, Presentable};
4use crate::shell::error::CliError;
5use solana_tools_lite::constants::compute_budget;
6use solana_tools_lite::models::analysis::{AnalysisWarning, TokenProgramKind, TxAnalysis};
7use solana_tools_lite::utils::format_sol;
8
9/// Bundles analysis and an optional JSON summary payload.
10pub struct AnalysisPresenter<'a> {
11    pub analysis: Option<&'a TxAnalysis>,
12    pub summary_payload: Option<&'a str>,
13}
14
15impl Presentable for AnalysisPresenter<'_> {
16    fn present(
17        &self,
18        json: bool,
19        _show_secret: bool,
20        to_stderr: bool,
21    ) -> Result<(), CliError> {
22        if let Some(analysis) = self.analysis {
23            emit_summary(analysis);
24        }
25        
26        if json {
27            if let Some(payload) = self.summary_payload {
28                emit_line(payload, to_stderr);
29            }
30        }
31        Ok(())
32    }
33}
34
35fn emit_summary(analysis: &TxAnalysis) {
36    for (i, t) in analysis.transfers.iter().enumerate() {
37        eprintln!("==================================================");
38        eprintln!("Instruction #{}: System Program (Transfer)", i + 1);
39        eprintln!(
40            "  From:   {}{}",
41            t.from,
42            if t.from_is_signer { " (signer)" } else { "" }
43        );
44        eprintln!("  To:     {}", t.to);
45        eprintln!("  Amount: {} ({} lamports)", format_sol(t.lamports as u128), t.lamports);
46        // eprintln!("          ({} lamports)", t.lamports);
47    }
48
49    eprintln!("--------------------------------------------------");
50    eprintln!("TRANSACTION SUMMARY");
51    eprintln!("Non-SOL Assets: {}", if analysis.has_non_sol_assets { "Yes (SPL/Token-2022 detected)" } else { "No" });
52    eprintln!(
53        "Network Fee:    {} ({} lamports)",
54        format_sol(analysis.base_fee_lamports),
55        analysis.base_fee_lamports
56    );
57    
58    if analysis.is_fee_payer {
59        eprintln!("                !!! YOU ARE THE FEE PAYER !!!");
60    }
61    
62    if let Some((pf, est)) = analysis.priority_fee_lamports {
63        if est {
64            eprintln!(
65                "Priority Fee:   {} ({} lamports, estimated with default {} CU)",
66                format_sol(pf),
67                pf,
68                compute_budget::DEFAULT_COMPUTE_UNIT_LIMIT
69            );
70        } else {
71            eprintln!(
72                "Priority Fee:   {} ({} lamports)",
73                format_sol(pf),
74                pf
75            );
76        }
77    } else {
78        eprintln!("Priority Fee:   {} (0 lamports)", format_sol(0));
79    }
80    
81    if let Some(price) = analysis.compute_unit_price_micro {
82        let limit = analysis
83            .compute_unit_limit
84            .unwrap_or(compute_budget::DEFAULT_COMPUTE_UNIT_LIMIT);
85        eprintln!(
86            "Compute Budget: price={} micro-lamports, limit={}",
87            price, limit
88        );
89    }
90    let total_cost = analysis.total_fee_lamports + analysis.total_sol_send_by_signer;
91    
92    if analysis.total_sol_send_by_signer > 0 {
93        eprintln!(
94            "YOU SEND:       {} ({} lamports)",
95            format_sol(analysis.total_sol_send_by_signer as u128),
96            analysis.total_sol_send_by_signer
97        );
98    }
99    
100    eprintln!("MAX TOTAL COST: {}", format_sol(total_cost));
101    
102    let (label, desc) = analysis.privacy_level.display_info(
103        analysis.confidential_ops_count,
104        analysis.storage_ops_count
105    );
106    eprintln!("PRIVACY LEVEL:  {} ({})", label, desc);
107    eprintln!("--------------------------------------------------");
108
109    if analysis.confidential_ops_count > 0 || analysis.storage_ops_count > 0 {
110        eprintln!("EXTENSION PROTOCOLS SUMMARY:");
111        if analysis.confidential_ops_count > 0 {
112            eprintln!("  - Private (Confidential) Operations: {}", analysis.confidential_ops_count);
113        }
114        if analysis.storage_ops_count > 0 {
115            eprintln!("  - Storage/Bridge (Public->ZK) Operations: {}", analysis.storage_ops_count);
116        }
117        eprintln!("--------------------------------------------------");
118    }
119
120    // Extension Protocol Actions
121    if !analysis.extension_actions.is_empty() {
122        eprintln!("EXTENSION PROTOCOLS DETECTED:");
123        for action in &analysis.extension_actions {
124            eprintln!("  - {}: {}", action.protocol_name(), action.description());
125        }
126        eprintln!("--------------------------------------------------");
127    }
128
129    // Protocol-specific Notices (Plugins)
130    if !analysis.extension_notices.is_empty() {
131        for notice in &analysis.extension_notices {
132            eprintln!("{}", notice);
133            eprintln!("--------------------------------------------------");
134        }
135    }
136
137    if !analysis.warnings.is_empty() {
138        eprintln!("WARNINGS:");
139        for w in &analysis.warnings {
140            eprintln!("- {}", warning_to_message(w));
141        }
142        eprintln!("--------------------------------------------------");
143    }
144}
145
146fn warning_to_message(warning: &AnalysisWarning) -> String {
147    match warning {
148        AnalysisWarning::LookupTableNotProvided => {
149            "Address table lookups present but lookup table was not provided; some accounts may be unresolved".to_string()
150        }
151        AnalysisWarning::LookupTableMissing(key) => {
152            format!("Lookup table {} missing or incomplete; some accounts may be unresolved", key)
153        }
154        AnalysisWarning::TokenTransferDetected(kind) => {
155            let label = match kind {
156                TokenProgramKind::SplToken => "Token Program",
157                TokenProgramKind::Token2022 => "Token-2022 Program",
158                TokenProgramKind::AssociatedToken => "Associated Token Program",
159            };
160            format!(
161                "{} interaction detected. Detailed token transfer amounts are not displayed in offline mode.",
162                label
163            )
164        }
165        AnalysisWarning::UnknownProgram { program_id } => {
166            format!("Unknown program encountered: {}", program_id)
167        }
168        AnalysisWarning::SignerNotRequired => {
169            "!!! SECURITY WARNING !!! Your signature is NOT REQUIRED for this transaction. This might be a phishing attempt if you were asked to sign it.".to_string()
170        }
171        AnalysisWarning::CpiLimit => {
172            "Analysis limited to top-level instructions. CPI (Cross-Program Invocations) not analyzed.".to_string()
173        }
174        AnalysisWarning::ConfidentialTransferDetected => {
175            "Confidential Transfer (Token-2022) detected. Transaction privacy level set to Hybrid/Confidential.".to_string()
176        }
177        AnalysisWarning::MalformedInstruction => {
178            "One or more protocol instructions are malformed (too short or corrupted data)".to_string()
179        }
180    }
181}