Skip to main content

simulator_api/reroute_report/
render.rs

1//! The report as a table and as JSON.
2
3use super::flow::{Direction, Flow, Report, fold};
4use crate::mints::short_mint;
5
6/// A longtail book reaches the hundreds, and the tail is individually negligible but
7/// collectively not, so the rest fold into one line rather than being dropped.
8const MAX_DIRECTIONS: usize = 12;
9
10impl Report {
11    /// The gap between the columns includes the reroute effect, not just any configured change.
12    ///
13    /// `subtitle` prints beside the venue name — the slot range, say, which this module cannot
14    /// know.
15    pub fn render(&self, subtitle: Option<&str>) -> String {
16        let target = self.target();
17        let mut out = String::new();
18        let name = target
19            .label()
20            .map(str::to_string)
21            .or_else(|| target.program().map(|program| program.to_string()))
22            .unwrap_or_else(|| "venue".to_string());
23        out.push_str(&format!("\n  {name}"));
24        if let Some(subtitle) = subtitle {
25            out.push_str(&format!("   {subtitle}"));
26        }
27        out.push_str("\n\n");
28
29        let priced = self.sol_usd().is_some();
30        out.push_str(&row("", "on L1", "re-quoted", "delta"));
31        let total = self.total();
32        out.push_str(&rows_for(&total, "", priced));
33        out.push_str(&share_row(&total, ""));
34
35        let mut ranked: Vec<_> = self
36            .by_direction()
37            .filter(|(_, direction)| direction.present())
38            .collect();
39        ranked.sort_unstable_by_key(|(mints, direction)| {
40            (std::cmp::Reverse(direction.rank()), *mints)
41        });
42        for ((input, output), direction) in ranked.iter().take(MAX_DIRECTIONS) {
43            out.push_str(&format!(
44                "\n  {} → {}\n",
45                short_mint(input),
46                short_mint(output)
47            ));
48            out.push_str(&rows_for(direction, "  ", priced));
49            out.push_str(&share_row(direction, "  "));
50        }
51        if let Some(rest) = ranked.get(MAX_DIRECTIONS..).filter(|rest| !rest.is_empty()) {
52            let tail = fold(rest.iter().map(|(_, direction)| **direction));
53            out.push_str(&format!("\n  + {} more directions\n", rest.len()));
54            out.push_str(&rows_for(&tail, "  ", priced));
55            out.push_str(&share_row(&tail, "  "));
56        }
57
58        out
59    }
60
61    /// Kept flat so several arms join as rows.
62    pub fn to_json(&self) -> serde_json::Value {
63        let target = self.target();
64        let total = self.total();
65        let directions: Vec<_> = self
66            .by_direction()
67            .map(|((input, output), direction)| {
68                serde_json::json!({
69                    "inputMint": input,
70                    "outputMint": output,
71                    "l1": flow_json(&direction.l1),
72                    "requoted": flow_json(&direction.requoted),
73                    "sharePct": direction.share().map(|(l1, _)| l1),
74                    "requotedSharePct": direction.share().map(|(_, requoted)| requoted),
75                })
76            })
77            .collect();
78        serde_json::json!({
79            "label": target.label(),
80            "program": target.program().map(|program| program.to_string()),
81            "total": {
82                "l1": flow_json(&total.l1),
83                "requoted": flow_json(&total.requoted),
84            },
85            // Per-leg normalized, unlike the per-hop `usd` above.
86            "share": {
87                "l1Pct": total.share().map(|(l1, _)| l1),
88                "requotedPct": total.share().map(|(_, requoted)| requoted),
89            },
90            "byDirection": directions,
91        })
92    }
93}
94
95fn flow_json(flow: &Flow) -> serde_json::Value {
96    serde_json::json!({
97        "swaps": flow.legs,
98        "usd": flow.usd,
99        "swapsWithoutAmount": flow.without_amount,
100        "swapsWithoutPrice": flow.without_price,
101    })
102}
103
104fn share_row(direction: &Direction, indent: &str) -> String {
105    let Some((l1, requoted)) = direction.share() else {
106        return String::new();
107    };
108    row(
109        &format!("{indent}share"),
110        &format!("{l1:.1}%"),
111        &format!("{requoted:.1}%"),
112        &format!("{:+.1}pp", requoted - l1),
113    )
114}
115
116fn row(label: &str, l1: &str, requoted: &str, delta: &str) -> String {
117    format!("  {label:<10} {l1:>14} {requoted:>14} {delta:>14}\n")
118}
119
120/// Legs and dollars only: each direction's amount is in its own input mint, and dollars are the
121/// only figure that crosses them.
122fn rows_for(direction: &Direction, indent: &str, priced: bool) -> String {
123    let mut out = totals("swaps", indent, direction.l1.legs, direction.requoted.legs);
124    if priced && !direction.fully_unpriced() {
125        out.push_str(&usd_row(direction, indent));
126    }
127    if direction.unpriced() {
128        out.push_str(&format!(
129            "{indent}  ({} swaps in mints this range could not price)\n",
130            direction.l1.without_price + direction.requoted.without_price
131        ));
132    }
133    out
134}
135
136fn totals(label: &str, indent: &str, l1: u64, requoted: u64) -> String {
137    row(
138        &format!("{indent}{label}"),
139        &l1.to_string(),
140        &requoted.to_string(),
141        &signed(i128::from(requoted) - i128::from(l1)),
142    )
143}
144
145fn usd_row(direction: &Direction, indent: &str) -> String {
146    row(
147        &format!("{indent}volume"),
148        &money(direction.l1.usd),
149        &money(direction.requoted.usd),
150        &format!(
151            "{}{}",
152            if direction.requoted.usd < direction.l1.usd {
153                "-"
154            } else {
155                "+"
156            },
157            money((direction.requoted.usd - direction.l1.usd).abs())
158        ),
159    )
160}
161
162fn money(usd: f64) -> String {
163    match usd {
164        usd if usd >= 1e6 => format!("${:.2}M", usd / 1e6),
165        usd if usd >= 1e3 => format!("${:.1}k", usd / 1e3),
166        usd => format!("${usd:.0}"),
167    }
168}
169
170fn signed(delta: i128) -> String {
171    match delta < 0 {
172        true => delta.to_string(),
173        false => format!("+{delta}"),
174    }
175}