1use std::collections::HashMap;
11
12use num_bigint::BigUint;
13use tycho_simulation::tycho_common::{models::Address, simulation::protocol_sim::ProtocolSim};
14
15use crate::{
16 algorithm::{sim_guard::GuardedProtocolSim, split_primitives::split_amount},
17 feed::market_data::MarketState,
18 types::{ComponentId, Route, Swap},
19};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct RouteReplay {
24 pub amount_out: BigUint,
26 pub gas: BigUint,
28}
29
30#[derive(Debug, thiserror::Error)]
32pub enum ReplayError {
33 #[error("route has no swaps")]
36 EmptyRoute,
37 #[error("no simulation state for component {0}")]
40 MissingState(ComponentId),
41 #[error("token {0} missing from the market state")]
43 MissingToken(Address),
44 #[error("simulation failed on component {component_id}: {error}")]
46 Simulation {
47 component_id: ComponentId,
49 error: String,
51 },
52}
53
54pub fn replay_route(route: &Route, market: &MarketState) -> Result<RouteReplay, ReplayError> {
69 let swaps = route.swaps();
70 let (Some(input_token), Some(output_token)) = (route.input_token(), route.output_token())
71 else {
72 return Err(ReplayError::EmptyRoute);
73 };
74 let total_in: BigUint = swaps
75 .iter()
76 .filter(|swap| *swap.token_in() == input_token)
77 .map(Swap::amount_in)
78 .sum();
79
80 let mut available: HashMap<Address, BigUint> = HashMap::new();
84 available.insert(input_token, total_in);
85 let mut branch_totals: HashMap<Address, BigUint> = HashMap::new();
86 let mut post_swap: HashMap<ComponentId, Box<dyn ProtocolSim>> = HashMap::new();
87 let mut total_gas = BigUint::ZERO;
88
89 for swap in swaps {
90 let token_in = market
91 .get_token(swap.token_in())
92 .ok_or_else(|| ReplayError::MissingToken(swap.token_in().clone()))?;
93 let token_out = market
94 .get_token(swap.token_out())
95 .ok_or_else(|| ReplayError::MissingToken(swap.token_out().clone()))?;
96
97 let branch_total = branch_totals
98 .entry(swap.token_in().clone())
99 .or_insert_with(|| {
100 available
101 .get(swap.token_in())
102 .cloned()
103 .unwrap_or_default()
104 })
105 .clone();
106 let remaining = available
107 .entry(swap.token_in().clone())
108 .or_default();
109 let amount_in = if *swap.split() > 0.0 {
110 let (part, _) = split_amount(&branch_total, *swap.split());
113 part.min(remaining.clone())
114 } else {
115 remaining.clone()
116 };
117 *remaining -= &amount_in;
118
119 let sim = post_swap
120 .get(swap.component_id())
121 .map(|state| state.as_ref())
122 .or_else(|| market.get_simulation_state(swap.component_id()))
123 .ok_or_else(|| ReplayError::MissingState(swap.component_id().to_string()))?;
124 let result = sim
125 .get_amount_out_guarded(amount_in, token_in, token_out)
126 .map_err(|e| ReplayError::Simulation {
127 component_id: swap.component_id().to_string(),
128 error: e.to_string(),
129 })?;
130
131 total_gas += &result.gas;
132 *available
133 .entry(swap.token_out().clone())
134 .or_default() += &result.amount;
135 post_swap.insert(swap.component_id().to_string(), result.new_state);
136 }
137
138 let amount_out = available
139 .remove(&output_token)
140 .unwrap_or_default();
141 Ok(RouteReplay { amount_out, gas: total_gas })
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::algorithm::test_utils::{component, token, ConstantProductSim, MockProtocolSim};
148
149 fn make_market(
150 pools: Vec<(
151 &str,
152 Vec<tycho_simulation::tycho_common::models::token::Token>,
153 Box<dyn ProtocolSim>,
154 )>,
155 ) -> MarketState {
156 let mut market = MarketState::new();
157 for (pool_id, tokens, sim) in pools {
158 market.upsert_components(std::iter::once(component(pool_id, &tokens)));
159 market.update_states([(pool_id.to_string(), sim)]);
160 market.upsert_tokens(tokens);
161 }
162 market
163 }
164
165 fn route_swap(
168 pool_id: &str,
169 token_in: &tycho_simulation::tycho_common::models::token::Token,
170 token_out: &tycho_simulation::tycho_common::models::token::Token,
171 amount_in: u64,
172 split: f64,
173 ) -> Swap {
174 Swap::new(
175 pool_id.to_string(),
176 "mock".to_string(),
177 token_in.address.clone(),
178 token_out.address.clone(),
179 BigUint::from(amount_in),
180 BigUint::ZERO,
181 BigUint::ZERO,
182 component(pool_id, &[token_in.clone(), token_out.clone()]),
183 Box::new(MockProtocolSim::new(1_000_000.0)),
184 )
185 .with_split(split)
186 }
187
188 fn route(swaps: Vec<Swap>) -> Route {
189 Route::new(swaps, HashMap::new()).expect("test route must not be empty")
190 }
191
192 #[test]
193 fn sequential_route_threads_amounts_through_market_state() {
194 let token_a = token(0x0A, "A");
197 let token_b = token(0x0B, "B");
198 let token_c = token(0x0C, "C");
199 let market = make_market(vec![
200 (
201 "pool_ab",
202 vec![token_a.clone(), token_b.clone()],
203 Box::new(MockProtocolSim::new(2.0).with_gas(50_000)),
204 ),
205 (
206 "pool_bc",
207 vec![token_b.clone(), token_c.clone()],
208 Box::new(MockProtocolSim::new(3.0).with_gas(70_000)),
209 ),
210 ]);
211 let route = route(vec![
212 route_swap("pool_ab", &token_a, &token_b, 1_000, 0.0),
213 route_swap("pool_bc", &token_b, &token_c, 2_000, 0.0),
214 ]);
215
216 let replay = replay_route(&route, &market).unwrap();
217 assert_eq!(replay.amount_out, BigUint::from(6_000u64));
218 assert_eq!(replay.gas, BigUint::from(120_000u64));
219 }
220
221 #[test]
222 fn split_route_divides_by_fraction_with_remainder() {
223 let token_a = token(0x0A, "A");
226 let token_b = token(0x0B, "B");
227 let market = make_market(vec![
228 ("pool_1", vec![token_a.clone(), token_b.clone()], Box::new(MockProtocolSim::new(2.0))),
229 ("pool_2", vec![token_a.clone(), token_b.clone()], Box::new(MockProtocolSim::new(3.0))),
230 ]);
231 let route = route(vec![
232 route_swap("pool_1", &token_a, &token_b, 600, 0.6),
233 route_swap("pool_2", &token_a, &token_b, 400, 0.0),
234 ]);
235
236 let replay = replay_route(&route, &market).unwrap();
237 assert_eq!(replay.amount_out, BigUint::from(2_400u64));
238 }
239
240 #[test]
241 fn splits_are_fractions_of_the_collected_total_not_the_remainder() {
242 let token_a = token(0x0A, "A");
245 let token_b = token(0x0B, "B");
246 let market = make_market(vec![
247 ("pool_1", vec![token_a.clone(), token_b.clone()], Box::new(MockProtocolSim::new(1.0))),
248 ("pool_2", vec![token_a.clone(), token_b.clone()], Box::new(MockProtocolSim::new(2.0))),
249 ("pool_3", vec![token_a.clone(), token_b.clone()], Box::new(MockProtocolSim::new(4.0))),
250 ]);
251 let route = route(vec![
252 route_swap("pool_1", &token_a, &token_b, 500, 0.5),
253 route_swap("pool_2", &token_a, &token_b, 300, 0.3),
254 route_swap("pool_3", &token_a, &token_b, 200, 0.0),
255 ]);
256
257 let replay = replay_route(&route, &market).unwrap();
259 assert_eq!(replay.amount_out, BigUint::from(1_900u64));
260 }
261
262 #[test]
263 fn shared_pool_sees_depleted_reserves() {
264 let token_a = token(0x0A, "A");
267 let token_b = token(0x0B, "B");
268 let cp = ConstantProductSim {
269 reserve_0: BigUint::from(10_000u64),
270 reserve_1: BigUint::from(10_000u64),
271 gas: 50_000,
272 };
273 let market = make_market(vec![(
274 "pool",
275 vec![token_a.clone(), token_b.clone()],
276 Box::new(cp.clone()),
277 )]);
278 let route = route(vec![
279 route_swap("pool", &token_a, &token_b, 500, 0.5),
280 route_swap("pool", &token_a, &token_b, 500, 0.0),
281 ]);
282
283 let replay = replay_route(&route, &market).unwrap();
284 let full_swap = cp
285 .get_amount_out(BigUint::from(1_000u64), &token_a, &token_b)
286 .unwrap()
287 .amount;
288 let diff = if replay.amount_out > full_swap {
289 &replay.amount_out - &full_swap
290 } else {
291 &full_swap - &replay.amount_out
292 };
293 assert!(diff <= BigUint::from(2u32), "split through one pool must match one full swap");
294 }
295
296 #[test]
297 fn changed_market_state_changes_the_output() {
298 let token_a = token(0x0A, "A");
300 let token_b = token(0x0B, "B");
301 let market = make_market(vec![(
302 "pool",
303 vec![token_a.clone(), token_b.clone()],
304 Box::new(MockProtocolSim::new(2.5)),
305 )]);
306 let route = route(vec![route_swap("pool", &token_a, &token_b, 1_000, 0.0)]);
308
309 let replay = replay_route(&route, &market).unwrap();
310 assert_eq!(replay.amount_out, BigUint::from(2_500u64));
311 }
312
313 #[test]
314 fn missing_simulation_state_errors() {
315 let token_a = token(0x0A, "A");
316 let token_b = token(0x0B, "B");
317 let mut market = make_market(vec![]);
318 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
319 let route = route(vec![route_swap("gone", &token_a, &token_b, 1_000, 0.0)]);
320
321 let err = replay_route(&route, &market).unwrap_err();
322 assert!(matches!(err, ReplayError::MissingState(id) if id == "gone"));
323 }
324
325 #[test]
326 fn missing_token_errors() {
327 let token_a = token(0x0A, "A");
328 let token_b = token(0x0B, "B");
329 let market = make_market(vec![(
330 "pool",
331 vec![token_a.clone(), token_b.clone()],
332 Box::new(MockProtocolSim::new(2.0)),
333 )]);
334 let unknown = token(0x42, "X");
335 let route = route(vec![route_swap("pool", &unknown, &token_b, 1_000, 0.0)]);
336
337 let err = replay_route(&route, &market).unwrap_err();
338 assert!(matches!(err, ReplayError::MissingToken(addr) if addr == unknown.address));
339 }
340}