simulator_api/
route_plan.rs1use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct RoutePlan(Vec<RouteHop>);
20
21impl RoutePlan {
22 pub fn new(hops: Vec<RouteHop>) -> Option<Self> {
24 (!hops.is_empty()).then_some(Self(hops))
25 }
26
27 pub fn hops(&self) -> &[RouteHop] {
28 &self.0
29 }
30
31 pub fn is_empty(&self) -> bool {
32 self.0.is_empty()
33 }
34
35 pub fn is_split(&self) -> bool {
36 is_split(&self.0)
37 }
38}
39
40pub fn is_split(hops: &[RouteHop]) -> bool {
43 hops.iter().any(|hop| hop.percent != FULL_PERCENT)
44}
45
46pub fn pool_keys(hops: &[RouteHop]) -> impl Iterator<Item = &str> {
49 hops.iter().filter_map(RouteHop::amm_key)
50}
51
52pub fn labels(hops: &[RouteHop]) -> impl Iterator<Item = &str> {
53 hops.iter().filter_map(RouteHop::label)
54}
55
56pub const FULL_PERCENT: u64 = 100;
58
59const fn full_percent() -> u64 {
60 FULL_PERCENT
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct RouteHop {
66 #[serde(default)]
68 pub leg: Option<u32>,
69 #[serde(default = "full_percent")]
71 pub percent: u64,
72 #[serde(default)]
74 pub program: Option<String>,
75 #[serde(default)]
77 pub bps: Option<u64>,
78 pub swap_info: RouteHopSwapInfo,
79}
80
81impl Default for RouteHop {
83 fn default() -> Self {
84 Self {
85 leg: None,
86 percent: FULL_PERCENT,
87 program: None,
88 bps: None,
89 swap_info: RouteHopSwapInfo::default(),
90 }
91 }
92}
93
94impl RouteHop {
95 pub fn label(&self) -> Option<&str> {
96 self.swap_info.label.as_deref()
97 }
98
99 pub fn amm_key(&self) -> Option<&str> {
100 self.swap_info.amm_key.as_deref()
101 }
102
103 pub fn input_mint(&self) -> Option<&str> {
104 self.swap_info.input_mint.as_deref()
105 }
106
107 pub fn output_mint(&self) -> Option<&str> {
108 self.swap_info.output_mint.as_deref()
109 }
110}
111
112#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct RouteHopSwapInfo {
115 #[serde(default)]
118 pub label: Option<String>,
119 #[serde(default)]
121 pub amm_key: Option<String>,
122 #[serde(default)]
123 pub input_mint: Option<String>,
124 #[serde(default)]
125 pub in_amount: Option<String>,
126 #[serde(default)]
127 pub output_mint: Option<String>,
128 #[serde(default)]
129 pub out_amount: Option<String>,
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn hop(percent: u64) -> RouteHop {
137 RouteHop {
138 percent,
139 ..Default::default()
140 }
141 }
142
143 #[test]
144 fn no_hops_is_absent_rather_than_empty() {
145 assert_eq!(RoutePlan::new(Vec::new()), None);
146 assert_eq!(
147 RoutePlan::new(vec![hop(FULL_PERCENT)]).map(|plan| plan.hops().len()),
148 Some(1)
149 );
150 }
151
152 #[test]
153 fn a_route_is_split_when_any_hop_carries_less_than_the_whole_trade() {
154 assert!(
155 !RoutePlan::new(vec![hop(100), hop(100)])
156 .expect("a plan")
157 .is_split()
158 );
159 assert!(
160 RoutePlan::new(vec![hop(60), hop(40)])
161 .expect("a plan")
162 .is_split()
163 );
164 }
165
166 #[test]
168 fn both_sources_serialize_onto_one_shape() {
169 let quoted = RouteHop {
170 swap_info: RouteHopSwapInfo {
171 label: Some("SolFi".to_string()),
172 amm_key: Some("pool".to_string()),
173 input_mint: Some("sol".to_string()),
174 in_amount: Some("1000".to_string()),
175 output_mint: Some("usdc".to_string()),
176 out_amount: Some("1010".to_string()),
177 },
178 ..Default::default()
179 };
180 let executed = RouteHop {
181 leg: Some(0),
182 program: Some("prog".to_string()),
183 swap_info: RouteHopSwapInfo {
184 amm_key: None,
185 ..quoted.swap_info.clone()
186 },
187 ..quoted.clone()
188 };
189
190 let json = |hop: &RouteHop| serde_json::to_value(hop).expect("json");
191 for value in [json("ed), json(&executed)] {
192 assert_eq!(value["percent"], 100);
193 assert_eq!(value["swapInfo"]["label"], "SolFi");
194 assert_eq!(value["swapInfo"]["inputMint"], "sol");
195 assert_eq!(value["swapInfo"]["inAmount"], "1000");
197 assert_eq!(value["swapInfo"]["outAmount"], "1010");
198 }
199 assert_eq!(json("ed)["swapInfo"]["ammKey"], "pool");
200 assert_eq!(json(&executed)["program"], "prog");
201 }
202
203 #[test]
205 fn a_recorded_plan_reads_back_onto_the_paths_it_was_written_with() {
206 let recorded = r#"[{"percent":100,"swapInfo":{"label":"Aquifer","ammKey":"CNC5Tae","inputMint":"EPjFWdd","outputMint":"So11111","inAmount":"385638781","outAmount":"4999931316"},"bps":null}]"#;
207
208 let plan: RoutePlan = serde_json::from_str(recorded).expect("a recorded plan decodes");
209 let rewritten = serde_json::to_value(&plan).expect("json");
210
211 assert_eq!(rewritten[0]["percent"], 100);
212 assert_eq!(rewritten[0]["swapInfo"]["label"], "Aquifer");
213 assert_eq!(rewritten[0]["swapInfo"]["ammKey"], "CNC5Tae");
214 assert_eq!(rewritten[0]["swapInfo"]["inputMint"], "EPjFWdd");
215 assert_eq!(rewritten[0]["swapInfo"]["inAmount"], "385638781");
216 assert_eq!(rewritten[0]["swapInfo"]["outputMint"], "So11111");
217 assert_eq!(rewritten[0]["swapInfo"]["outAmount"], "4999931316");
218 assert_eq!(rewritten[0]["bps"], serde_json::Value::Null);
219 }
220
221 #[test]
224 fn an_unset_field_is_written_as_null_rather_than_omitted() {
225 let value = serde_json::to_value(hop(FULL_PERCENT)).expect("json");
226
227 assert!(value.get("program").is_some(), "written, not skipped");
228 assert_eq!(value["program"], serde_json::Value::Null);
229 assert_eq!(value["swapInfo"]["ammKey"], serde_json::Value::Null);
230 assert_eq!(value["program"], value["neverWritten"]);
231 }
232}