1use std::collections::HashMap;
2use std::fmt::{Display, Formatter};
3use std::sync::Arc;
4
5use crate::Swap;
6use alloy_primitives::utils::format_units;
7use alloy_primitives::{Address, U256};
8use eyre::{OptionExt, Result, eyre};
9use kabu_evm_utils::NWETH;
10use kabu_types_market::Token;
11use lazy_static::lazy_static;
12use rand::random;
13use tracing::{error, info};
14
15#[derive(Clone, Debug)]
16pub struct Tips {
17 pub token_in: Arc<Token>,
18 pub profit: U256,
19 pub profit_eth: U256,
20 pub tips: U256,
21 pub min_change: U256,
22}
23
24impl Display for Tips {
25 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
26 write!(
27 f,
28 "{} : tips {} min_change {} profit : {} eth : {} ",
29 self.token_in.get_symbol(),
30 format_units(self.tips, "ether").unwrap_or_default(),
31 self.token_in.to_float(self.min_change),
32 self.token_in.to_float(self.profit),
33 format_units(self.profit_eth, "ether").unwrap_or_default(),
34 )
35 }
36}
37
38lazy_static! {
39 static ref START_PCT : U256 = U256::from(9900);
40
41 static ref SLOPES : Vec<(U256,U256)> = vec![
42(U256::from(10).pow(U256::from(19)), U256::from(7000)),
44 (U256::from(10).pow(U256::from(19))*U256::from(5), U256::from(5000))
45 ];
46}
47pub fn tips_pct_advanced(profit: &U256) -> u32 {
48 let mut start_point = U256::ZERO;
49 let mut start_pct = *START_PCT;
50 for (x, y) in SLOPES.iter() {
51 if x > profit {
52 return (start_pct - ((start_pct - y) * (profit - start_point) / (x - start_point))).to::<u32>();
53 }
54 start_point = *x;
55 start_pct = *y;
56 }
57 start_pct.to()
58}
59
60pub fn randomize_tips_pct(tips_pct: u32) -> u32 {
61 let rnd: u32 = random::<u32>() % 50;
62 tips_pct - rnd
63}
64
65pub fn tips_and_value_for_swap_type(
66 swap: &Swap,
67 tips_pct: Option<u32>,
68 gas_cost: Option<U256>,
69 eth_balance: U256,
70) -> Result<(Vec<Tips>, U256)> {
71 let total_profit_eth = swap.arb_profit_eth();
72 info!("Total profit eth : {}", format_units(total_profit_eth, "ether").unwrap_or_default());
73 let tips_pct = randomize_tips_pct(tips_pct.unwrap_or(tips_pct_advanced(&total_profit_eth)));
74
75 if let Some(gas_cost) = gas_cost
76 && total_profit_eth < gas_cost
77 {
78 info!(
79 "total_profit_eth={} < {}",
80 format_units(total_profit_eth, "ether").unwrap_or_default(),
81 format_units(gas_cost, "ether").unwrap_or_default()
82 );
83 return Err(eyre!("NOT_ENOUGH_PROFIT"));
84 }
85
86 match swap {
87 Swap::BackrunSwapLine(_) | Swap::BackrunSwapSteps(_) => {
88 let profit = swap.arb_profit();
89 if profit.is_zero() {
90 error!(profit = NWETH::to_float(profit), %swap, "Zero profit");
91 return Err(eyre!("NO_PROFIT"));
92 }
93 let token_in = swap.get_first_token().ok_or_eyre("NO_FIRST_TOKEN")?.clone();
94 let profit_eth = token_in.calc_eth_value(profit).ok_or_eyre("CALC_ETH_VALUE_FAILED")?;
95
96 if let Some(gas_cost) = gas_cost
97 && profit_eth < gas_cost
98 {
99 error!(
100 profit_eth = NWETH::to_float(profit_eth),
101 gas_cost = NWETH::to_float(gas_cost),
102 %swap,
103 "Profit doesn't exceed the gas cost"
104 );
105 return Err(eyre!("NO_PROFIT_EXCEEDING_GAS"));
106 }
107
108 let mut tips = profit_eth.checked_sub(gas_cost.unwrap_or_default()).ok_or_eyre("SUBTRACTION_OVERFLOWN")? * U256::from(tips_pct)
109 / U256::from(10000);
110 let min_change = token_in.calc_token_value_from_eth(gas_cost.unwrap_or_default() + tips).unwrap();
111 let mut value = if token_in.is_weth() { U256::ZERO } else { tips };
112
113 if !token_in.is_weth() && (tips > ((eth_balance * U256::from(9000)) / U256::from(10000))) {
114 tips = (eth_balance * U256::from(9000)) / U256::from(10000);
115 value = tips;
116 }
117
118 Ok((vec![Tips { token_in, profit, profit_eth, tips, min_change }], value))
119 }
120 Swap::Multiple(swap_vec) => {
121 let mut tips_hashset: HashMap<Address, Tips> = HashMap::new();
122
123 let profit_eth = swap.arb_profit_eth();
124
125 if let Some(gas_cost) = gas_cost
126 && profit_eth < gas_cost
127 {
128 error!(
129 profit_eth = NWETH::to_float(profit_eth),
130 gas_cost = NWETH::to_float(gas_cost),
131 %swap,
132 "Profit doesn't exceed the gas cost"
133 );
134 return Err(eyre!("NO_PROFIT_EXCEEDING_GAS"));
135 }
136
137 let gas_cost_per_record = gas_cost.unwrap_or_default() / U256::from(swap_vec.len());
138
139 for swap_record in swap_vec.iter() {
140 let token_in = swap_record.get_first_token().ok_or_eyre("NO_FIRST_TOKEN")?.clone();
141
142 let profit = swap_record.arb_profit();
143 if profit.is_zero() {
144 error!(profit = NWETH::to_float(profit), %swap, "Zero profit");
145 return Err(eyre!("NO_PROFIT"));
146 }
147
148 let profit_eth = token_in.calc_eth_value(profit).ok_or_eyre("CALC_ETH_VALUE_FAILED")?;
149
150 let tips = profit_eth.checked_sub(gas_cost_per_record).ok_or_eyre("SUBTRACTION_OVERFLOWN")? * U256::from(tips_pct)
151 / U256::from(10000);
152 let min_change = token_in.calc_token_value_from_eth(tips + gas_cost_per_record).unwrap();
153
154 let entry = tips_hashset.entry(token_in.get_address()).or_insert(Tips {
155 token_in,
156 profit: U256::ZERO,
157 profit_eth: U256::ZERO,
158 tips: U256::ZERO,
159 min_change: U256::ZERO,
160 });
161
162 entry.profit += profit;
163 entry.profit_eth += profit_eth;
164 entry.tips += tips;
165 entry.min_change += min_change;
166 }
167
168 let mut value = U256::ZERO;
169
170 if tips_hashset.iter().any(|(_, x)| x.token_in.is_weth()) {
171 let total_tips_eth: U256 = tips_hashset.iter().filter(|(_, x)| x.token_in.is_weth()).map(|(_, x)| x.tips).sum();
172 let total_tips_non_eth: U256 = tips_hashset.iter().filter(|(_, x)| !x.token_in.is_weth()).map(|(_, x)| x.tips).sum();
173 let total_profit_eth: U256 = tips_hashset.iter().filter(|(_, x)| x.token_in.is_weth()).map(|(_, x)| x.profit_eth).sum();
174 for (_, token_tips) in tips_hashset.iter_mut() {
175 if token_tips.token_in.is_weth() {
176 token_tips.tips = total_tips_eth + total_tips_non_eth;
177 if total_tips_eth + total_tips_non_eth > total_profit_eth {
178 value = total_tips_eth + total_tips_non_eth - total_profit_eth;
179 }
180
181 if value > eth_balance {
182 token_tips.tips = token_tips.tips.checked_sub(value).ok_or_eyre("SUBTRACTION_OVERFLOWN")?;
183 value = eth_balance * U256::from(9000) / U256::from(10000);
184 token_tips.tips += value;
185 }
186 } else {
187 token_tips.tips = U256::ZERO;
188 }
189 }
190 } else {
191 let total_tips = tips_hashset.values().map(|x| x.tips).sum::<U256>();
192 value = if total_tips >= eth_balance { eth_balance * U256::from(9000) / U256::from(10000) } else { total_tips };
193
194 for (idx, (_, token_tips)) in tips_hashset.iter_mut().enumerate() {
195 token_tips.tips = if idx == 0 { value } else { U256::ZERO };
196 }
197 }
198 let tips_vec = tips_hashset.values().cloned().collect();
199
200 Ok((tips_vec, value + U256::from(100)))
201 }
202
203 _ => Err(eyre!("NOT_IMPLEMENTED")),
204 }
205}