use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RoutePlan(Vec<RouteHop>);
impl RoutePlan {
pub fn new(hops: Vec<RouteHop>) -> Option<Self> {
(!hops.is_empty()).then_some(Self(hops))
}
pub fn hops(&self) -> &[RouteHop] {
&self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn is_split(&self) -> bool {
is_split(&self.0)
}
}
pub fn is_split(hops: &[RouteHop]) -> bool {
hops.iter().any(|hop| hop.percent != FULL_PERCENT)
}
pub fn pool_keys(hops: &[RouteHop]) -> impl Iterator<Item = &str> {
hops.iter().filter_map(RouteHop::amm_key)
}
pub fn labels(hops: &[RouteHop]) -> impl Iterator<Item = &str> {
hops.iter().filter_map(RouteHop::label)
}
pub const FULL_PERCENT: u64 = 100;
const fn full_percent() -> u64 {
FULL_PERCENT
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RouteHop {
#[serde(default)]
pub leg: Option<u32>,
#[serde(default = "full_percent")]
pub percent: u64,
#[serde(default)]
pub program: Option<String>,
#[serde(default)]
pub bps: Option<u64>,
pub swap_info: RouteHopSwapInfo,
}
impl Default for RouteHop {
fn default() -> Self {
Self {
leg: None,
percent: FULL_PERCENT,
program: None,
bps: None,
swap_info: RouteHopSwapInfo::default(),
}
}
}
impl RouteHop {
pub fn label(&self) -> Option<&str> {
self.swap_info.label.as_deref()
}
pub fn amm_key(&self) -> Option<&str> {
self.swap_info.amm_key.as_deref()
}
pub fn input_mint(&self) -> Option<&str> {
self.swap_info.input_mint.as_deref()
}
pub fn output_mint(&self) -> Option<&str> {
self.swap_info.output_mint.as_deref()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RouteHopSwapInfo {
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub amm_key: Option<String>,
#[serde(default)]
pub input_mint: Option<String>,
#[serde(default)]
pub in_amount: Option<String>,
#[serde(default)]
pub output_mint: Option<String>,
#[serde(default)]
pub out_amount: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn hop(percent: u64) -> RouteHop {
RouteHop {
percent,
..Default::default()
}
}
#[test]
fn no_hops_is_absent_rather_than_empty() {
assert_eq!(RoutePlan::new(Vec::new()), None);
assert_eq!(
RoutePlan::new(vec![hop(FULL_PERCENT)]).map(|plan| plan.hops().len()),
Some(1)
);
}
#[test]
fn a_route_is_split_when_any_hop_carries_less_than_the_whole_trade() {
assert!(
!RoutePlan::new(vec![hop(100), hop(100)])
.expect("a plan")
.is_split()
);
assert!(
RoutePlan::new(vec![hop(60), hop(40)])
.expect("a plan")
.is_split()
);
}
#[test]
fn both_sources_serialize_onto_one_shape() {
let quoted = RouteHop {
swap_info: RouteHopSwapInfo {
label: Some("SolFi".to_string()),
amm_key: Some("pool".to_string()),
input_mint: Some("sol".to_string()),
in_amount: Some("1000".to_string()),
output_mint: Some("usdc".to_string()),
out_amount: Some("1010".to_string()),
},
..Default::default()
};
let executed = RouteHop {
leg: Some(0),
program: Some("prog".to_string()),
swap_info: RouteHopSwapInfo {
amm_key: None,
..quoted.swap_info.clone()
},
..quoted.clone()
};
let json = |hop: &RouteHop| serde_json::to_value(hop).expect("json");
for value in [json("ed), json(&executed)] {
assert_eq!(value["percent"], 100);
assert_eq!(value["swapInfo"]["label"], "SolFi");
assert_eq!(value["swapInfo"]["inputMint"], "sol");
assert_eq!(value["swapInfo"]["inAmount"], "1000");
assert_eq!(value["swapInfo"]["outAmount"], "1010");
}
assert_eq!(json("ed)["swapInfo"]["ammKey"], "pool");
assert_eq!(json(&executed)["program"], "prog");
}
#[test]
fn a_recorded_plan_reads_back_onto_the_paths_it_was_written_with() {
let recorded = r#"[{"percent":100,"swapInfo":{"label":"Aquifer","ammKey":"CNC5Tae","inputMint":"EPjFWdd","outputMint":"So11111","inAmount":"385638781","outAmount":"4999931316"},"bps":null}]"#;
let plan: RoutePlan = serde_json::from_str(recorded).expect("a recorded plan decodes");
let rewritten = serde_json::to_value(&plan).expect("json");
assert_eq!(rewritten[0]["percent"], 100);
assert_eq!(rewritten[0]["swapInfo"]["label"], "Aquifer");
assert_eq!(rewritten[0]["swapInfo"]["ammKey"], "CNC5Tae");
assert_eq!(rewritten[0]["swapInfo"]["inputMint"], "EPjFWdd");
assert_eq!(rewritten[0]["swapInfo"]["inAmount"], "385638781");
assert_eq!(rewritten[0]["swapInfo"]["outputMint"], "So11111");
assert_eq!(rewritten[0]["swapInfo"]["outAmount"], "4999931316");
assert_eq!(rewritten[0]["bps"], serde_json::Value::Null);
}
#[test]
fn an_unset_field_is_written_as_null_rather_than_omitted() {
let value = serde_json::to_value(hop(FULL_PERCENT)).expect("json");
assert!(value.get("program").is_some(), "written, not skipped");
assert_eq!(value["program"], serde_json::Value::Null);
assert_eq!(value["swapInfo"]["ammKey"], serde_json::Value::Null);
assert_eq!(value["program"], value["neverWritten"]);
}
}