scope/cli/
address_report.rs1use super::address::AddressReport;
6use crate::display::report::{report_footer, save_report};
7use crate::error::Result;
8use chrono::{DateTime, Utc};
9use std::path::Path;
10
11pub fn generate_address_report(report: &AddressReport) -> String {
13 generate_address_report_core(report, true, true)
14}
15
16pub fn generate_address_report_section(report: &AddressReport) -> String {
18 generate_address_report_core(report, false, false)
19}
20
21pub fn generate_dossier_report(
24 report: &AddressReport,
25 risk: &crate::compliance::risk::RiskAssessment,
26) -> String {
27 let mut md = String::new();
28 md.push_str("# Wallet Dossier\n\n");
29 md.push_str(&format!(
30 "**Address:** `{}` \n**Chain:** {} \n**Generated:** {} \n\n",
31 report.address,
32 capitalize_chain(&report.chain),
33 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
34 ));
35 md.push_str("---\n\n");
36 md.push_str(&report_balance(report));
37 md.push_str("\n---\n\n");
38 md.push_str(&report_transactions(report));
39 md.push_str("\n---\n\n");
40 md.push_str(&report_tokens(report));
41 md.push_str("\n---\n\n");
42 md.push_str("## Risk Assessment\n\n");
43 md.push_str(&crate::display::format_risk_report(
44 risk,
45 crate::display::OutputFormat::Markdown,
46 true,
47 ));
48 md.push_str(&report_footer());
49 md
50}
51
52fn generate_address_report_core(
53 report: &AddressReport,
54 include_header: bool,
55 include_footer: bool,
56) -> String {
57 let mut md = String::new();
58
59 if include_header {
60 md.push_str(&report_header(report));
61 md.push_str("\n---\n\n");
62 }
63 md.push_str(&report_balance(report));
64 md.push_str("\n---\n\n");
65 md.push_str(&report_transactions(report));
66 md.push_str("\n---\n\n");
67 md.push_str(&report_tokens(report));
68 if include_footer {
69 md.push_str(&report_footer());
70 }
71
72 md
73}
74
75fn report_header(report: &AddressReport) -> String {
76 format!(
77 "# Address Analysis Report\n\n\
78 **Address:** `{}` \n\
79 **Chain:** {} \n\
80 **Generated:** {} \n",
81 report.address,
82 capitalize_chain(&report.chain),
83 Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
84 )
85}
86
87fn report_balance(report: &AddressReport) -> String {
88 let mut s = String::from("## Balance Summary\n\n");
89 s.push_str("| Metric | Value |\n|--------|-------|\n");
90 s.push_str(&format!(
91 "| Native Balance | {} |\n",
92 report.balance.formatted
93 ));
94 if let Some(usd) = report.balance.usd {
95 s.push_str(&format!("| USD Value | ${:.2} |\n", usd));
96 }
97 s.push_str(&format!(
98 "| Transaction Count | {} |\n",
99 report.transaction_count
100 ));
101 s
102}
103
104fn report_transactions(report: &AddressReport) -> String {
105 let mut s = String::from("## Recent Transactions\n\n");
106 match &report.transactions {
107 Some(txs) if !txs.is_empty() => {
108 s.push_str("| Hash | Block | Time | From | To | Value | Status |\n");
109 s.push_str("|------|-------|------|------|-----|-------|--------|\n");
110 for tx in txs.iter().take(20) {
111 let ts = DateTime::<Utc>::from_timestamp(tx.timestamp as i64, 0)
112 .map(|d| d.format("%Y-%m-%d %H:%M").to_string())
113 .unwrap_or_else(|| "-".to_string());
114 let hash_short = if tx.hash.len() > 10 {
115 format!("{}...{}", &tx.hash[..6], &tx.hash[tx.hash.len() - 4..])
116 } else {
117 tx.hash.clone()
118 };
119 let to = tx.to.as_deref().unwrap_or("-");
120 let status = if tx.status { "✓" } else { "✗" };
121 s.push_str(&format!(
122 "| `{}` | {} | {} | `{}` | `{}` | {} | {} |\n",
123 hash_short, tx.block_number, ts, tx.from, to, tx.value, status
124 ));
125 }
126 if txs.len() > 20 {
127 s.push_str(&format!("\n*Showing 20 of {} transactions*\n", txs.len()));
128 }
129 }
130 _ => s.push_str("*No transaction data available*\n"),
131 }
132 s
133}
134
135fn report_tokens(report: &AddressReport) -> String {
136 let mut s = String::from("## Token Balances\n\n");
137 match &report.tokens {
138 Some(tokens) if !tokens.is_empty() => {
139 s.push_str("| Token | Contract | Balance |\n");
140 s.push_str("|-------|----------|--------|\n");
141 for t in tokens {
142 s.push_str(&format!(
143 "| {} ({}) | `{}` | {} |\n",
144 t.name, t.symbol, t.contract_address, t.formatted_balance
145 ));
146 }
147 }
148 _ => s.push_str("*No token balance data available*\n"),
149 }
150 s
151}
152
153fn capitalize_chain(chain: &str) -> String {
154 let mut chars = chain.chars();
155 match chars.next() {
156 None => String::new(),
157 Some(c) => c.to_uppercase().chain(chars).collect(),
158 }
159}
160
161pub fn save_address_report(report: &str, path: impl AsRef<Path>) -> Result<()> {
163 save_report(report, path)
164}