1use std::io::{self, Write};
2
3use serde_json::{Map, Value, json};
4
5use crate::diff::{AccountDiff, AccountStatus, StateDiff, Summary, TxDiff};
6use crate::primitives::{B256, Delta, U256, encode_hex};
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
10pub enum Format {
11 #[default]
12 Text,
13 Json,
14 Ndjson,
15 Csv,
16}
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
20pub enum Unit {
21 #[default]
22 Ether,
23 Wei,
24}
25
26#[derive(Clone, Debug)]
28pub enum Report {
29 Net { label: String, transactions: usize, diff: StateDiff },
30 PerTx(Vec<TxDiff>),
31}
32
33pub fn render(report: &Report, format: Format, unit: Unit, out: &mut impl Write) -> io::Result<()> {
34 match format {
35 Format::Text => text(report, unit, out),
36 Format::Json => {
37 serde_json::to_writer_pretty(&mut *out, &json_report(report))?;
38 writeln!(out)
39 }
40 Format::Ndjson => {
41 for row in rows(report) {
42 serde_json::to_writer(&mut *out, &row.to_json())?;
43 writeln!(out)?;
44 }
45 Ok(())
46 }
47 Format::Csv => {
48 writeln!(out, "tx,address,status,kind,slot,from,to,delta")?;
49 for row in rows(report) {
50 writeln!(
51 out,
52 "{},{},{},{},{},{},{},{}",
53 row.tx.unwrap_or_default(),
54 row.address,
55 row.status,
56 row.kind,
57 row.slot.unwrap_or_default(),
58 row.from,
59 row.to,
60 row.delta.unwrap_or_default()
61 )?;
62 }
63 Ok(())
64 }
65 }
66}
67
68fn status_name(s: AccountStatus) -> &'static str {
69 match s {
70 AccountStatus::Created => "created",
71 AccountStatus::Destroyed => "destroyed",
72 AccountStatus::Modified => "modified",
73 }
74}
75
76fn compact(word: &B256) -> String {
78 U256::from_be_slice(&word.0).expect("32 bytes").to_hex_quantity()
79}
80
81fn amount(v: U256, unit: Unit) -> String {
82 match unit {
83 Unit::Ether => format!("{} ETH", v.format_ether()),
84 Unit::Wei => format!("{v} wei"),
85 }
86}
87
88fn signed(d: Delta, unit: Unit) -> String {
89 match unit {
90 Unit::Ether => format!("{} ETH", d.format_ether()),
91 Unit::Wei => format!("{d} wei"),
92 }
93}
94
95fn text(report: &Report, unit: Unit, out: &mut impl Write) -> io::Result<()> {
96 match report {
97 Report::Net { label, transactions, diff } => {
98 let plural = if *transactions == 1 { "" } else { "s" };
99 writeln!(out, "== {label} ({transactions} transaction{plural})")?;
100 text_diff(diff, unit, out)
101 }
102 Report::PerTx(txs) => {
103 for (i, tx) in txs.iter().enumerate() {
104 if i > 0 {
105 writeln!(out)?;
106 }
107 match tx.tx_hash {
108 Some(h) => writeln!(out, "== tx {h}")?,
109 None => writeln!(out, "== tx #{i}")?,
110 }
111 text_diff(&tx.diff, unit, out)?;
112 }
113 Ok(())
114 }
115 }
116}
117
118fn text_diff(diff: &StateDiff, unit: Unit, out: &mut impl Write) -> io::Result<()> {
119 if diff.is_empty() {
120 return writeln!(out, "no state changes");
121 }
122 for (addr, acct) in &diff.accounts {
123 writeln!(out, "{addr} {}", status_name(acct.status))?;
124 if let (Some(c), Some(d)) = (acct.balance, acct.balance_delta()) {
125 writeln!(
126 out,
127 " balance {} -> {} ({})",
128 amount(c.from, unit),
129 amount(c.to, unit),
130 signed(d, unit)
131 )?;
132 }
133 if let Some(c) = acct.nonce {
134 writeln!(out, " nonce {} -> {}", c.from, c.to)?;
135 }
136 if let Some(c) = &acct.code {
137 writeln!(out, " code {} bytes -> {} bytes", c.from.len(), c.to.len())?;
138 } else if let Some(c) = acct.code_hash {
139 writeln!(out, " codehash {} -> {}", c.from, c.to)?;
140 }
141 for (slot, c) in &acct.storage {
142 writeln!(out, " storage {}: {} -> {}", compact(slot), compact(&c.from), compact(&c.to))?;
143 }
144 }
145 let s = diff.summary();
146 writeln!(
147 out,
148 "summary: {} accounts ({} created, {} destroyed); {} balance, {} nonce, {} code, {} storage changes",
149 s.accounts,
150 s.created,
151 s.destroyed,
152 s.balance_changes,
153 s.nonce_changes,
154 s.code_changes,
155 s.storage_changes
156 )
157}
158
159fn summary_json(s: &Summary) -> Value {
160 json!({
161 "accounts": s.accounts,
162 "created": s.created,
163 "destroyed": s.destroyed,
164 "balance_changes": s.balance_changes,
165 "nonce_changes": s.nonce_changes,
166 "code_changes": s.code_changes,
167 "storage_changes": s.storage_changes,
168 "total_credited_wei": s.total_credited.to_string(),
169 "total_debited_wei": s.total_debited.to_string(),
170 })
171}
172
173fn account_json(acct: &AccountDiff) -> Value {
174 let mut o = Map::new();
175 o.insert("status".into(), json!(status_name(acct.status)));
176 if let (Some(c), Some(d)) = (acct.balance, acct.balance_delta()) {
177 o.insert(
178 "balance".into(),
179 json!({"from": c.from.to_string(), "to": c.to.to_string(), "delta": d.to_string()}),
180 );
181 }
182 if let Some(c) = acct.nonce {
183 o.insert("nonce".into(), json!({"from": c.from, "to": c.to}));
184 }
185 if let Some(c) = &acct.code {
186 o.insert(
187 "code".into(),
188 json!({"from": c.from.to_string(), "to": c.to.to_string(), "from_len": c.from.len(), "to_len": c.to.len()}),
189 );
190 }
191 if let Some(c) = acct.code_hash {
192 o.insert("code_hash".into(), json!({"from": c.from.to_string(), "to": c.to.to_string()}));
193 }
194 if !acct.storage.is_empty() {
195 let slots: Map<String, Value> = acct
196 .storage
197 .iter()
198 .map(|(slot, c)| (slot.to_string(), json!({"from": c.from.to_string(), "to": c.to.to_string()})))
199 .collect();
200 o.insert("storage".into(), Value::Object(slots));
201 }
202 Value::Object(o)
203}
204
205fn diff_json(diff: &StateDiff) -> Value {
206 let accounts: Map<String, Value> =
207 diff.accounts.iter().map(|(a, acct)| (a.to_string(), account_json(acct))).collect();
208 json!({"summary": summary_json(&diff.summary()), "accounts": accounts})
209}
210
211fn json_report(report: &Report) -> Value {
212 match report {
213 Report::Net { label, transactions, diff } => {
214 let mut v = diff_json(diff);
215 v["source"] = json!(label);
216 v["transactions"] = json!(transactions);
217 v
218 }
219 Report::PerTx(txs) => json!({
220 "transactions": txs.iter().map(|t| {
221 let mut v = diff_json(&t.diff);
222 v["tx_hash"] = t.tx_hash.map_or(Value::Null, |h| json!(h.to_string()));
223 v
224 }).collect::<Vec<_>>()
225 }),
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct Row {
232 pub tx: Option<String>,
233 pub address: String,
234 pub status: &'static str,
235 pub kind: &'static str,
236 pub slot: Option<String>,
237 pub from: String,
238 pub to: String,
239 pub delta: Option<String>,
240}
241
242impl Row {
243 fn to_json(&self) -> Value {
244 let mut v = json!({
245 "tx": self.tx,
246 "address": self.address,
247 "status": self.status,
248 "kind": self.kind,
249 "from": self.from,
250 "to": self.to,
251 });
252 if let Some(slot) = &self.slot {
253 v["slot"] = json!(slot);
254 }
255 if let Some(delta) = &self.delta {
256 v["delta"] = json!(delta);
257 }
258 v
259 }
260}
261
262fn diff_rows(tx: Option<String>, diff: &StateDiff, out: &mut Vec<Row>) {
263 for (addr, acct) in &diff.accounts {
264 let row = |kind, slot, from: String, to: String, delta| Row {
265 tx: tx.clone(),
266 address: addr.to_string(),
267 status: status_name(acct.status),
268 kind,
269 slot,
270 from,
271 to,
272 delta,
273 };
274 if let (Some(c), Some(d)) = (acct.balance, acct.balance_delta()) {
275 out.push(row("balance", None, c.from.to_string(), c.to.to_string(), Some(d.to_string())));
276 }
277 if let Some(c) = acct.nonce {
278 out.push(row("nonce", None, c.from.to_string(), c.to.to_string(), None));
279 }
280 if let Some(c) = &acct.code {
281 out.push(row("code", None, encode_hex(&c.from.0), encode_hex(&c.to.0), None));
282 }
283 if let Some(c) = acct.code_hash {
284 out.push(row("code_hash", None, c.from.to_string(), c.to.to_string(), None));
285 }
286 for (slot, c) in &acct.storage {
287 out.push(row("storage", Some(slot.to_string()), c.from.to_string(), c.to.to_string(), None));
288 }
289 if !acct.has_changes() {
290 out.push(row("account", None, String::new(), String::new(), None));
291 }
292 }
293}
294
295pub fn rows(report: &Report) -> Vec<Row> {
296 let mut out = Vec::new();
297 match report {
298 Report::Net { diff, .. } => diff_rows(None, diff, &mut out),
299 Report::PerTx(txs) => {
300 for t in txs {
301 diff_rows(t.tx_hash.map(|h| h.to_string()), &t.diff, &mut out);
302 }
303 }
304 }
305 out
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::trace::parse_traces_str;
312
313 fn report() -> Report {
314 let json = r#"{"pre":{
315 "0x0101010101010101010101010101010101010101":{"balance":"0xde0b6b3a7640000","nonce":1,"storage":{"0x1":"0x7"}}},
316 "post":{
317 "0x0101010101010101010101010101010101010101":{"balance":"0x6f05b59d3b20000","nonce":2,"storage":{"0x2":"0x1"}},
318 "0x0202020202020202020202020202020202020202":{"balance":"0x6f05b59d3b20000"}}}"#;
319 let diff = StateDiff::from_prestate(&parse_traces_str(json).unwrap()[0].diff);
320 Report::Net { label: "example".into(), transactions: 1, diff }
321 }
322
323 fn render_str(format: Format, unit: Unit) -> String {
324 let mut buf = Vec::new();
325 render(&report(), format, unit, &mut buf).unwrap();
326 String::from_utf8(buf).unwrap()
327 }
328
329 #[test]
330 fn text_output() {
331 let s = render_str(Format::Text, Unit::Ether);
332 assert!(s.starts_with("== example (1 transaction)\n"));
333 assert!(s.contains(" balance 1 ETH -> 0.5 ETH (-0.5 ETH)\n"));
334 assert!(s.contains(" nonce 1 -> 2\n"));
335 assert!(s.contains(" storage 0x1: 0x7 -> 0x0\n"));
336 assert!(s.contains(" storage 0x2: 0x0 -> 0x1\n"));
337 assert!(s.contains("0x0202020202020202020202020202020202020202 created\n"));
338 assert!(s.ends_with(
339 "summary: 2 accounts (1 created, 0 destroyed); 2 balance, 1 nonce, 0 code, 2 storage changes\n"
340 ));
341 assert!(render_str(Format::Text, Unit::Wei).contains("(-500000000000000000 wei)"));
342 }
343
344 #[test]
345 fn json_output() {
346 let v: Value = serde_json::from_str(&render_str(Format::Json, Unit::Ether)).unwrap();
347 assert_eq!(v["transactions"], 1);
348 assert_eq!(v["summary"]["accounts"], 2);
349 assert_eq!(v["summary"]["total_credited_wei"], "500000000000000000");
350 let a = &v["accounts"]["0x0101010101010101010101010101010101010101"];
351 assert_eq!(a["balance"]["delta"], "-500000000000000000");
352 assert_eq!(a["nonce"]["to"], 2);
353 assert_eq!(
354 a["storage"]["0x0000000000000000000000000000000000000000000000000000000000000001"]["to"],
355 "0x0000000000000000000000000000000000000000000000000000000000000000"
356 );
357 }
358
359 #[test]
360 fn ndjson_and_csv_rows() {
361 let nd = render_str(Format::Ndjson, Unit::Ether);
362 let lines: Vec<Value> = nd.lines().map(|l| serde_json::from_str(l).unwrap()).collect();
363 assert_eq!(lines.len(), 5);
364 assert!(lines.iter().all(|l| l["tx"].is_null()));
365 let csv = render_str(Format::Csv, Unit::Ether);
366 let mut it = csv.lines();
367 assert_eq!(it.next(), Some("tx,address,status,kind,slot,from,to,delta"));
368 assert_eq!(it.count(), 5);
369 assert!(csv.contains(",balance,,1000000000000000000,500000000000000000,-500000000000000000"));
370 }
371
372 #[test]
373 fn empty_diff() {
374 let r = Report::PerTx(vec![TxDiff { tx_hash: None, diff: StateDiff::default() }]);
375 let mut buf = Vec::new();
376 render(&r, Format::Text, Unit::Ether, &mut buf).unwrap();
377 assert_eq!(String::from_utf8(buf).unwrap(), "== tx #0\nno state changes\n");
378 }
379}