use std::collections::BTreeMap;
use solana_address::Address;
use super::{LegRecord, Prices, Samples};
use crate::route_plan::{RouteHop, RoutePlan};
fn in_amount(hop: &RouteHop) -> Option<u128> {
hop.swap_info.in_amount.as_ref()?.parse().ok()
}
#[derive(Debug, thiserror::Error)]
pub enum RerouteReportError {
#[error(
"{asked} never appears in this stream.\nIt names {} venues; the busiest are {}.",
.venues.len(),
busiest(.venues),
)]
NothingMatched {
asked: String,
venues: Vec<(String, u64)>,
},
}
const NAMED_IN_ERROR: usize = 8;
fn busiest(venues: &[(String, u64)]) -> String {
let mut ranked: Vec<_> = venues.iter().collect();
ranked.sort_unstable_by(|(a_label, a_hops), (b_label, b_hops)| {
b_hops.cmp(a_hops).then_with(|| a_label.cmp(b_label))
});
ranked
.iter()
.take(NAMED_IN_ERROR)
.map(|(label, hops)| format!("{label} ({hops})"))
.collect::<Vec<_>>()
.join(", ")
}
#[derive(Clone, Default, Debug)]
pub struct Target {
label: Option<String>,
program: Option<(Address, String)>,
}
impl Target {
pub fn new(label: Option<String>, program: Option<Address>) -> Self {
Self {
label,
program: program.map(|program| (program, program.to_string())),
}
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn program(&self) -> Option<Address> {
self.program.as_ref().map(|(program, _)| *program)
}
pub fn claims_quoted(&self, hop: &RouteHop) -> bool {
match &self.label {
Some(label) => hop.label() == Some(label.as_str()),
None => false,
}
}
pub fn claims_fill(&self, hop: &RouteHop) -> bool {
if let Some((_, key)) = &self.program {
return hop.program.as_deref() == Some(key.as_str());
}
self.claims_quoted(hop)
}
fn describe(&self) -> String {
if let Some(label) = &self.label {
format!("label {label:?}")
} else if let Some((program, _)) = &self.program {
format!("program {program}")
} else {
"that venue".to_string()
}
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct Flow {
pub legs: u64,
pub usd: f64,
pub without_amount: u64,
pub without_price: u64,
share_usd: f64,
}
impl Flow {
fn merge(&mut self, other: Self) {
self.legs += other.legs;
self.usd += other.usd;
self.without_amount += other.without_amount;
self.without_price += other.without_price;
self.share_usd += other.share_usd;
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct Direction {
pub l1: Flow,
pub requoted: Flow,
l1_all: f64,
requoted_all: f64,
}
impl Direction {
fn merge(&mut self, other: Self) {
self.l1.merge(other.l1);
self.requoted.merge(other.requoted);
self.l1_all += other.l1_all;
self.requoted_all += other.requoted_all;
}
pub fn share(&self) -> Option<(f64, f64)> {
(self.l1_all > 0.0 && self.requoted_all > 0.0).then(|| {
(
self.l1.share_usd / self.l1_all * 100.0,
self.requoted.share_usd / self.requoted_all * 100.0,
)
})
}
pub const fn present(&self) -> bool {
self.l1.legs > 0 || self.requoted.legs > 0
}
pub const fn unpriced(&self) -> bool {
self.l1.without_price > 0 || self.requoted.without_price > 0
}
pub fn fully_unpriced(&self) -> bool {
self.unpriced() && self.l1.usd == 0.0 && self.requoted.usd == 0.0
}
pub(super) fn rank(&self) -> (u64, u64) {
let usd = self.l1.usd.max(self.requoted.usd);
(usd as u64, self.l1.legs.max(self.requoted.legs))
}
}
pub(super) fn fold(directions: impl IntoIterator<Item = Direction>) -> Direction {
directions
.into_iter()
.fold(Direction::default(), |mut total, direction| {
total.merge(direction);
total
})
}
pub type Pair = (String, String);
#[derive(Clone, Copy)]
enum Side {
L1,
Requoted,
}
#[derive(Debug)]
pub struct Report {
by_direction: BTreeMap<Pair, Direction>,
target: Target,
sol_usd: Option<f64>,
}
impl Report {
pub fn from_records<'a>(
target: Target,
records: impl IntoIterator<Item = &'a LegRecord>,
) -> Result<Self, RerouteReportError> {
let mut builder = ReportBuilder::new(target);
for record in records {
builder.push(record);
}
builder.finish()
}
pub const fn target(&self) -> &Target {
&self.target
}
pub const fn sol_usd(&self) -> Option<f64> {
self.sol_usd
}
pub fn by_direction(&self) -> impl Iterator<Item = (&Pair, &Direction)> {
self.by_direction.iter()
}
pub fn total(&self) -> Direction {
fold(self.by_direction.values().copied())
}
}
pub struct ReportBuilder {
target: Target,
samples: Samples,
by_direction: Directions,
labels: BTreeMap<String, u64>,
}
impl ReportBuilder {
pub fn new(target: Target) -> Self {
Self {
target,
samples: Samples::default(),
by_direction: Directions::default(),
labels: BTreeMap::new(),
}
}
pub fn push(&mut self, record: &LegRecord) {
self.samples.push(record);
if record.failed || record.original_failed {
return;
}
let Self {
target,
by_direction,
labels,
..
} = self;
fn hops_of(plan: &Option<RoutePlan>) -> Option<&[RouteHop]> {
plan.as_ref().map(RoutePlan::hops)
}
let quoted = hops_of(&record.route_plan);
for label in quoted.iter().copied().flatten().filter_map(RouteHop::label) {
match labels.get_mut(label) {
Some(hops) => *hops += 1,
None => {
labels.insert(label.to_string(), 1);
}
}
}
let (requoted, from_quote) = match hops_of(&record.realized_route_plan) {
Some(filled) => (filled, false),
None => (quoted.unwrap_or_default(), true),
};
let claimed = |hop: &RouteHop| match from_quote {
true => target.claims_quoted(hop),
false => target.claims_fill(hop),
};
for hop in requoted.iter().filter(|hop| claimed(hop)) {
by_direction.record(Side::Requoted, hop);
}
let l1 = hops_of(&record.original_route_plan);
for hop in l1
.iter()
.copied()
.flatten()
.filter(|hop| target.claims_fill(hop))
{
by_direction.record(Side::L1, hop);
}
let leg_amount = u128::from(record.amount) as f64;
by_direction.accumulate_share(
Side::Requoted,
requoted,
record.input_mint,
leg_amount,
claimed,
);
if let Some(l1) = l1 {
by_direction.accumulate_share(Side::L1, l1, record.input_mint, leg_amount, |hop| {
target.claims_fill(hop)
});
}
}
pub fn finish(self) -> Result<Report, RerouteReportError> {
let prices = self.samples.prices();
let report = Report {
sol_usd: prices.as_ref().map(Prices::sol_usd),
by_direction: self.by_direction.price(prices.as_ref()),
target: self.target,
};
let total = report.total();
if total.l1.legs == 0 && total.requoted.legs == 0 {
return Err(RerouteReportError::NothingMatched {
asked: report.target.describe(),
venues: self.labels.into_iter().collect(),
});
}
Ok(report)
}
}
#[derive(Default)]
struct Directions(BTreeMap<Pair, DirectionAccum>);
impl Directions {
fn record(&mut self, side: Side, hop: &RouteHop) {
let Some(key) = key_of(hop) else {
return;
};
let flow = &mut self.0.entry(key).or_default().side_mut(side).flow;
flow.legs += 1;
match in_amount(hop) {
Some(amount) => flow.raw += amount,
None => flow.without_amount += 1,
}
}
fn accumulate_share(
&mut self,
side: Side,
hops: &[RouteHop],
leg_mint: Address,
leg_amount: f64,
claimed: impl Fn(&RouteHop) -> bool,
) {
let total: u64 = hops.iter().map(|hop| hop.percent).sum();
if total == 0 {
return;
}
for hop in hops {
let Some(key) = key_of(hop) else {
continue;
};
let value = leg_amount * hop.percent as f64 / total as f64;
let share = &mut self.0.entry(key).or_default().side_mut(side).share;
credit(&mut share.all, leg_mint, value);
if claimed(hop) {
credit(&mut share.claimed, leg_mint, value);
}
}
}
fn price(self, prices: Option<&Prices>) -> BTreeMap<Pair, Direction> {
self.0
.into_iter()
.map(|(pair, accum)| {
let direction = accum.price(&pair.0, prices);
(pair, direction)
})
.filter(|(_, direction)| {
direction.present() || direction.l1_all > 0.0 || direction.requoted_all > 0.0
})
.collect()
}
}
fn key_of(hop: &RouteHop) -> Option<Pair> {
Some((
hop.input_mint()?.to_string(),
hop.output_mint()?.to_string(),
))
}
fn credit(bucket: &mut Vec<(Address, f64)>, mint: Address, value: f64) {
match bucket.iter_mut().find(|(held, _)| *held == mint) {
Some((_, total)) => *total += value,
None => bucket.push((mint, value)),
}
}
#[derive(Default)]
struct FlowAccum {
legs: u64,
raw: u128,
without_amount: u64,
}
impl FlowAccum {
fn price(self, mint: &str, prices: Option<&Prices>, share_usd: f64) -> Flow {
let usd = prices.and_then(|prices| prices.usd_str(mint, self.raw as f64));
Flow {
legs: self.legs,
usd: usd.unwrap_or_default(),
without_amount: self.without_amount,
without_price: match usd {
Some(_) => 0,
None => self.legs - self.without_amount,
},
share_usd,
}
}
}
#[derive(Default)]
struct ShareAccum {
all: Vec<(Address, f64)>,
claimed: Vec<(Address, f64)>,
}
impl ShareAccum {
fn price(&self, prices: Option<&Prices>) -> (f64, f64) {
let sum = |bucket: &[(Address, f64)]| {
bucket
.iter()
.filter_map(|(mint, raw)| prices?.usd(mint, *raw))
.fold(0.0, |total, usd| total + usd)
};
(sum(&self.all), sum(&self.claimed))
}
}
#[derive(Default)]
struct SideAccum {
flow: FlowAccum,
share: ShareAccum,
}
#[derive(Default)]
struct DirectionAccum {
l1: SideAccum,
requoted: SideAccum,
}
impl DirectionAccum {
fn side_mut(&mut self, side: Side) -> &mut SideAccum {
match side {
Side::L1 => &mut self.l1,
Side::Requoted => &mut self.requoted,
}
}
fn price(self, mint: &str, prices: Option<&Prices>) -> Direction {
let (l1_all, l1_claimed) = self.l1.share.price(prices);
let (requoted_all, requoted_claimed) = self.requoted.share.price(prices);
Direction {
l1: self.l1.flow.price(mint, prices, l1_claimed),
requoted: self.requoted.flow.price(mint, prices, requoted_claimed),
l1_all,
requoted_all,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
mints,
reroute_report::testing::{hop, plan, record_of},
};
const SOL: &str = "So11111111111111111111111111111111111111112";
const USDC: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const VENUE_PROGRAM: &str = "BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi";
const OTHER_PROGRAM: &str = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc";
fn anchors() -> Vec<LegRecord> {
(1..=3)
.map(|n| LegRecord {
original_realized_output_amount: Some(n * 100_000_000),
..record_of(mints::SOL, mints::USDC, n * 1_000_000_000, None, None, None)
})
.collect()
}
fn venue() -> Target {
Target::new(
Some("BisonFi".to_string()),
Some(VENUE_PROGRAM.parse().expect("a venue program")),
)
}
fn fill(
program: &str,
label: &str,
input: &str,
output: &str,
amount: u64,
) -> Option<RoutePlan> {
plan(vec![hop(Some(program), label, input, output, amount)])
}
fn report_of(records: &[LegRecord]) -> Report {
Report::from_records(venue(), records).expect("the venue appears")
}
fn priced(extra: impl IntoIterator<Item = LegRecord>) -> Vec<LegRecord> {
let mut records = anchors();
records.extend(extra);
records
}
fn won_leg(amount: u64) -> LegRecord {
let realized = fill(VENUE_PROGRAM, "BisonFi", SOL, USDC, amount);
let l1 = fill(OTHER_PROGRAM, "Whirlpool", SOL, USDC, amount);
record_of(mints::SOL, mints::USDC, amount, None, l1, realized)
}
#[test]
fn a_hop_with_an_unnamed_mint_opens_no_direction() {
let mut unnamed = hop(Some(VENUE_PROGRAM), "BisonFi", USDC, SOL, 1_000_000);
unnamed.swap_info.output_mint = None;
let l1 = plan(vec![
hop(Some(VENUE_PROGRAM), "BisonFi", SOL, USDC, 2_000_000_000),
unnamed,
]);
let report = report_of(&priced([record_of(
mints::SOL,
mints::USDC,
2_000_000_000,
None,
l1,
None,
)]));
let pairs: Vec<&Pair> = report.by_direction().map(|(pair, _)| pair).collect();
assert_eq!(pairs, [&(SOL.to_string(), USDC.to_string())]);
assert_eq!(report.total().l1.legs, 1, "only the named hop counts");
}
#[test]
fn deferring_the_conversion_matches_pricing_each_hop_as_it_arrives() {
let report = report_of(&priced([won_leg(2_000_000_000), won_leg(3_000_000_000)]));
let total = report.total();
assert_eq!(report.sol_usd(), Some(100.0));
assert_eq!(total.requoted.legs, 2);
assert_eq!(total.requoted.usd, 500.0);
assert_eq!(total.l1.legs, 0, "L1 had these legs at another venue");
}
#[test]
fn the_result_does_not_depend_on_arrival_order() {
let forward = priced([won_leg(2_000_000_000), won_leg(3_000_000_000)]);
let mut backward = forward.clone();
backward.reverse();
let (forward, backward) = (report_of(&forward), report_of(&backward));
assert_eq!(forward.sol_usd(), backward.sol_usd());
assert_eq!(forward.total().requoted.usd, backward.total().requoted.usd);
assert_eq!(forward.render(None), backward.render(None));
}
#[test]
fn neither_side_counts_flow_that_never_landed() {
let records = priced([
won_leg(2_000_000_000),
LegRecord {
failed: true,
..won_leg(9_000_000_000)
},
LegRecord {
original_failed: true,
..won_leg(9_000_000_000)
},
]);
let total = report_of(&records).total();
assert_eq!(total.requoted.legs, 1, "only the landed leg counts");
assert_eq!(total.requoted.usd, 200.0);
}
#[test]
fn a_share_splits_a_leg_where_notional_counts_it_twice() {
let two_hops = plan(vec![
hop(Some(VENUE_PROGRAM), "BisonFi", SOL, USDC, 1_000_000_000),
hop(Some(OTHER_PROGRAM), "Whirlpool", USDC, "BONK", 100_000_000),
]);
let l1 = fill(VENUE_PROGRAM, "BisonFi", SOL, USDC, 1_000_000_000);
let record = record_of(mints::SOL, mints::USDC, 1_000_000_000, None, l1, two_hops);
let report = report_of(&priced([record]));
let total = report.total();
assert_eq!(total.requoted.usd, 100.0);
assert_eq!(total.requoted_all, 100.0, "the leg is counted once");
assert_eq!(
total.requoted.share_usd, 50.0,
"half the route was the venue's"
);
assert_eq!(total.share(), Some((100.0, 50.0)));
let (_, first) = report
.by_direction()
.find(|((input, output), _)| input == SOL && output == USDC)
.expect("the venue's direction");
assert_eq!(first.requoted.share_usd, 50.0);
assert_eq!(first.requoted_all, 50.0, "that direction held half the leg");
}
#[test]
fn a_fill_prefers_the_program_and_falls_back_to_the_label() {
let filled = fill(VENUE_PROGRAM, "BisonFi", SOL, USDC, 1_000);
let record = record_of(mints::SOL, mints::USDC, 1_000, None, filled.clone(), filled);
let hop = &record.original_route_plan.as_ref().expect("a plan").hops()[0];
let by_label = Target::new(Some("BisonFi".to_string()), None);
assert!(by_label.claims_fill(hop));
assert!(by_label.claims_quoted(hop));
let other = Target::new(Some("BisonFi".to_string()), Some(Address::new_unique()));
assert!(!other.claims_fill(hop));
let by_program = Target::new(None, Some(VENUE_PROGRAM.parse().expect("a program")));
assert!(by_program.claims_fill(hop));
assert!(!by_program.claims_quoted(hop));
}
#[test]
fn the_report_is_swaps_and_dollars_only() {
let sell = fill(VENUE_PROGRAM, "BisonFi", SOL, USDC, 1_000_000_000);
let buy = fill(VENUE_PROGRAM, "BisonFi", USDC, SOL, 100_000_000);
let report = report_of(&priced([
record_of(mints::SOL, mints::USDC, 1_000_000_000, None, None, sell),
record_of(mints::USDC, mints::SOL, 100_000_000, None, None, buy),
]));
let json = report.to_json();
assert_eq!(json["total"]["l1"]["swaps"], 0);
assert_eq!(json["total"]["requoted"]["swaps"], 2);
assert_eq!(json["total"]["requoted"]["usd"], 200.0);
let table = report.render(None);
let totals = table.split('→').next().expect("a totals block");
assert!(totals.contains("swaps") && totals.contains("volume"));
assert!(!totals.contains("raw in"), "a raw amount reached the table");
assert!(!table.contains("quoted fill"));
}
#[test]
fn a_range_that_cannot_be_priced_reports_no_dollars() {
let report = report_of(&[won_leg(2_000_000_000)]);
let total = report.total();
assert_eq!(report.sol_usd(), None);
assert_eq!(total.requoted.legs, 1);
assert_eq!(total.requoted.without_price, 1);
assert!(total.fully_unpriced());
let table = report.render(None);
assert!(!table.contains("volume"), "a dollar row without a price");
assert!(table.contains("could not price"));
}
#[test]
fn a_selector_that_matches_nothing_says_so() {
let quote = plan(vec![hop(None, "Whirlpool", SOL, USDC, 1_000)]);
let records = vec![record_of(mints::SOL, mints::USDC, 1_000, quote, None, None)];
let target = Target::new(Some("Nope".to_string()), None);
let error = Report::from_records(target, &records).expect_err("the venue never appears");
let message = error.to_string();
assert!(message.contains("Nope"), "names what was asked for");
assert!(message.contains("Whirlpool (1)"));
}
#[test]
fn the_subtitle_is_the_callers() {
let report = report_of(&[won_leg(2_000_000_000)]);
assert!(report.render(Some("slots 1–100")).contains("slots 1–100"));
assert!(!report.render(None).contains("slots"));
}
}