kabu_strategy_merger/
swappath_merger_actor.rs

1use alloy_consensus::BlockHeader;
2use alloy_primitives::{Address, U256};
3use eyre::{Result, eyre};
4use kabu_core_components::Component;
5use reth_node_types::{HeaderTy, NodeTypesWithDB};
6use reth_provider::{BlockNumReader, HeaderProvider};
7use reth_tasks::TaskExecutor;
8use revm::{Database, DatabaseCommit, DatabaseRef};
9use tokio::sync::{broadcast, broadcast::error::RecvError};
10use tracing::{debug, error};
11
12use kabu_evm_db::KabuDBError;
13use kabu_types_events::{MarketEvents, MessageSwapCompose, SwapComposeData, SwapComposeMessage};
14use kabu_types_swap::{Swap, SwapStep};
15use revm::context::BlockEnv;
16use revm::context_interface::block::BlobExcessGasAndPrice;
17
18async fn arb_swap_steps_optimizer_task<
19    DB: DatabaseRef<Error = KabuDBError> + Database<Error = KabuDBError> + DatabaseCommit + Send + Sync + Clone,
20>(
21    compose_channel_tx: broadcast::Sender<MessageSwapCompose<DB>>,
22    _state_db: DB,
23    block_number: u64,
24    block_timestamp: u64,
25    request: SwapComposeData<DB>,
26) -> Result<()> {
27    debug!("Step Simulation started");
28
29    if let Swap::BackrunSwapSteps((sp0, sp1)) = request.swap {
30        let start_time = chrono::Local::now();
31
32        let _block_env = BlockEnv {
33            number: U256::from(block_number + 1),
34            timestamp: U256::from(block_timestamp + 12),
35            blob_excess_gas_and_price: Some(BlobExcessGasAndPrice { excess_blob_gas: 0, blob_gasprice: 0 }),
36            ..Default::default()
37        };
38
39        // TODO: Update optimize_swap_steps to work with KabuEVMWrapper
40        match Ok::<(SwapStep, SwapStep), eyre::Error>((sp0.clone(), sp1.clone())) {
41            Ok((s0, s1)) => {
42                let encode_request = MessageSwapCompose::prepare(SwapComposeData {
43                    origin: Some("merger_searcher".to_string()),
44                    tips_pct: None,
45                    swap: Swap::BackrunSwapSteps((s0, s1)),
46                    ..request
47                });
48                compose_channel_tx.send(encode_request).map_err(|_| eyre!("CANNOT_SEND"))?;
49            }
50            Err(e) => {
51                error!("Optimization error:{}", e);
52                return Err(eyre!("OPTIMIZATION_ERROR"));
53            }
54        }
55        debug!("Step Optimization finished {} + {} {}", &sp0, &sp1, chrono::Local::now() - start_time);
56    } else {
57        error!("Incorrect swap_type");
58        return Err(eyre!("INCORRECT_SWAP_TYPE"));
59    }
60
61    Ok(())
62}
63
64async fn arb_swap_path_merger_worker<
65    N: NodeTypesWithDB,
66    R: BlockNumReader + HeaderProvider<Header = HeaderTy<N>> + Send + Sync + Clone + 'static,
67    DB: DatabaseRef<Error = KabuDBError> + Database<Error = KabuDBError> + DatabaseCommit + Send + Sync + Clone + 'static,
68>(
69    provider: R,
70    multicaller_address: Address,
71    market_events_rx: broadcast::Sender<MarketEvents>,
72    compose_channel_rx: broadcast::Sender<MessageSwapCompose<DB>>,
73    compose_channel_tx: broadcast::Sender<MessageSwapCompose<DB>>,
74) -> Result<()>
75where
76    HeaderTy<N>: BlockHeader,
77{
78    let mut market_events_receiver = market_events_rx.subscribe();
79    let mut compose_channel_receiver = compose_channel_rx.subscribe();
80
81    let mut ready_requests: Vec<SwapComposeData<DB>> = Vec::new();
82    let mut processed_swaps = std::collections::HashSet::new();
83
84    loop {
85        tokio::select! {
86            msg = market_events_receiver.recv() => {
87                let msg : Result<MarketEvents, RecvError> = msg;
88                match msg {
89                    Ok(event) => {
90                        match event {
91                            MarketEvents::BlockHeaderUpdate{..} =>{
92                                debug!("Cleaning ready requests");
93                                ready_requests = Vec::new();
94                                processed_swaps.clear();
95                            }
96                            MarketEvents::BlockStateUpdate{..}=>{
97                                debug!("State updated");
98                                //state_db = market_state.read().await.state_db.clone();
99                            }
100                            _=>{}
101                        }
102                    }
103                    Err(e)=>{error!("{}", e)}
104                }
105
106            },
107            msg = compose_channel_receiver.recv() => {
108                let msg : Result<MessageSwapCompose<DB>, RecvError> = msg;
109                match msg {
110                    Ok(swap) => {
111
112                        let compose_data = match swap.inner() {
113                            SwapComposeMessage::Ready(data) => data,
114                            _=>continue,
115                        };
116
117                        let swap_path = match &compose_data.swap {
118                            Swap::BackrunSwapLine(path) => path,
119                            _=>continue,
120                        };
121
122                        // Create a unique key for this swap to detect duplicates
123                        let swap_key = format!("{:?}", compose_data.swap);
124
125                        // Skip if we've already processed this exact swap
126                        if processed_swaps.contains(&swap_key) {
127                            debug!("Skipping duplicate swap in merger");
128                            continue;
129                        }
130
131                        for req in ready_requests.iter() {
132
133                            let req_swap = match &req.swap {
134                                Swap::BackrunSwapLine(path)=>path,
135                                _ => continue,
136                            };
137
138                            // todo!() mega bundle merge
139                            if !compose_data.same_stuffing(&req.tx_compose.stuffing_txs_hashes) {
140                                continue
141                            };
142
143                            // Pre-filter: check if paths can be merged before attempting expensive merge
144                            if !SwapStep::can_merge_swap_paths(req_swap, swap_path) {
145                                continue;
146                            }
147
148                            match SwapStep::merge_swap_paths( req_swap.clone(), swap_path.clone(), multicaller_address ){
149                                Ok((sp0, sp1)) => {
150                                    // Get latest block number and timestamp from provider
151                                    let (block_number, block_timestamp) = match provider.last_block_number() {
152                                        Ok(block_num) => {
153                                            match provider.sealed_header(block_num) {
154                                                Ok(Some(sealed)) => {
155                                                    let header = sealed.header();
156                                                    (header.number(), header.timestamp())
157                                                },
158                                                Ok(None) => {
159                                                    error!("No header for block {}", block_num);
160                                                    continue;
161                                                }
162                                                Err(e) => {
163                                                    error!("Failed to get header for block {}: {}", block_num, e);
164                                                    continue;
165                                                }
166                                            }
167                                        }
168                                        Err(e) => {
169                                            error!("Failed to get last block number: {}", e);
170                                            continue;
171                                        }
172                                    };
173
174                                    let request = SwapComposeData{
175                                        swap : Swap::BackrunSwapSteps((sp0,sp1)),
176                                        ..compose_data.clone()
177                                    };
178
179                                    if let Some(db) = compose_data.poststate.clone() {
180                                        let db_clone = db.clone();
181
182                                        let compose_channel_clone = compose_channel_tx.clone();
183                                        tokio::task::spawn( async move {
184                                                arb_swap_steps_optimizer_task(
185                                                    compose_channel_clone,
186                                                    db_clone,
187                                                    block_number,
188                                                    block_timestamp,
189                                                    request
190                                                ).await
191                                        });
192                                    }
193                                    break; // only first
194                                }
195                                Err(e)=>{
196                                    error!("SwapPath merge error : {} {}", ready_requests.len(), e);
197                                }
198                            }
199                        }
200                        processed_swaps.insert(swap_key);
201                        ready_requests.push(compose_data.clone());
202                        ready_requests.sort_by(|r0,r1| r1.swap.arb_profit().cmp(&r0.swap.arb_profit())  );
203
204                        // Keep only top 20 most profitable swaps to prevent O(n²) comparison explosion
205                        if ready_requests.len() > 20 {
206                            ready_requests.truncate(20);
207                        }
208
209                    }
210                    Err(e)=>{error!("{}",e)}
211                }
212            }
213        }
214    }
215}
216
217#[derive(Clone)]
218pub struct ArbSwapPathMergerComponent<N, R, DB>
219where
220    N: NodeTypesWithDB,
221    R: BlockNumReader + HeaderProvider<Header = HeaderTy<N>> + Send + Sync + Clone + 'static,
222    DB: Send + Sync + Clone + 'static,
223{
224    provider: R,
225    multicaller_address: Address,
226    market_events: Option<broadcast::Sender<MarketEvents>>,
227    compose_channel_rx: Option<broadcast::Sender<MessageSwapCompose<DB>>>,
228    compose_channel_tx: Option<broadcast::Sender<MessageSwapCompose<DB>>>,
229    _phantom: std::marker::PhantomData<N>,
230}
231
232impl<N, R, DB> ArbSwapPathMergerComponent<N, R, DB>
233where
234    N: NodeTypesWithDB,
235    R: BlockNumReader + HeaderProvider<Header = HeaderTy<N>> + Send + Sync + Clone + 'static,
236    DB: DatabaseRef + Send + Sync + Clone + 'static,
237{
238    pub fn new(provider: R, multicaller_address: Address) -> ArbSwapPathMergerComponent<N, R, DB> {
239        ArbSwapPathMergerComponent {
240            provider,
241            multicaller_address,
242            market_events: None,
243            compose_channel_rx: None,
244            compose_channel_tx: None,
245            _phantom: std::marker::PhantomData,
246        }
247    }
248
249    pub fn with_market_events_channel(self, market_events: broadcast::Sender<MarketEvents>) -> Self {
250        Self { market_events: Some(market_events), ..self }
251    }
252
253    pub fn with_compose_channel(self, compose_channel: broadcast::Sender<MessageSwapCompose<DB>>) -> Self {
254        Self { compose_channel_rx: Some(compose_channel.clone()), compose_channel_tx: Some(compose_channel), ..self }
255    }
256}
257
258impl<N, R, DB> Component for ArbSwapPathMergerComponent<N, R, DB>
259where
260    N: NodeTypesWithDB,
261    R: BlockNumReader + HeaderProvider<Header = HeaderTy<N>> + Send + Sync + Clone + 'static,
262    DB: DatabaseRef<Error = KabuDBError> + Database<Error = KabuDBError> + DatabaseCommit + Send + Sync + Clone + 'static,
263    HeaderTy<N>: BlockHeader,
264{
265    fn spawn(self, executor: TaskExecutor) -> Result<()> {
266        let name = self.name();
267
268        let compose_channel_rx = self.compose_channel_rx.ok_or_else(|| eyre!("compose_channel_rx not set"))?;
269        let compose_channel_tx = self.compose_channel_tx.ok_or_else(|| eyre!("compose_channel_tx not set"))?;
270        let market_events_rx = self.market_events.ok_or_else(|| eyre!("market_events not set"))?;
271
272        executor.spawn_critical(name, async move {
273            if let Err(e) = arb_swap_path_merger_worker::<N, _, _>(
274                self.provider,
275                self.multicaller_address,
276                market_events_rx,
277                compose_channel_rx,
278                compose_channel_tx,
279            )
280            .await
281            {
282                error!("Arb swap path merger worker failed: {}", e);
283            }
284        });
285
286        Ok(())
287    }
288
289    fn name(&self) -> &'static str {
290        "ArbSwapPathMergerComponent"
291    }
292}
293
294#[cfg(test)]
295mod test {
296    use alloy_primitives::{Address, U256};
297    use kabu_evm_db::KabuDB;
298    use kabu_types_events::SwapComposeData;
299    use kabu_types_market::SwapPath;
300    use kabu_types_market::Token;
301    use kabu_types_swap::{Swap, SwapAmountType, SwapLine};
302    use std::sync::Arc;
303
304    #[test]
305    pub fn test_sort() {
306        let mut ready_requests: Vec<SwapComposeData<KabuDB>> = Vec::new();
307        let token = Arc::new(Token::new(Address::random()));
308
309        let sp0 = SwapLine {
310            path: SwapPath {
311                tokens: vec![token.clone(), token.clone()],
312                pools: vec![],
313                disabled: false,
314                disabled_pool: Default::default(),
315                score: None,
316            },
317            amount_in: SwapAmountType::Set(U256::from(1)),
318            amount_out: SwapAmountType::Set(U256::from(2)),
319            ..Default::default()
320        };
321        ready_requests.push(SwapComposeData { swap: Swap::BackrunSwapLine(sp0), ..SwapComposeData::default() });
322
323        let sp1 = SwapLine {
324            path: SwapPath {
325                tokens: vec![token.clone(), token.clone()],
326                pools: vec![],
327                disabled: false,
328                disabled_pool: Default::default(),
329                score: None,
330            },
331            amount_in: SwapAmountType::Set(U256::from(10)),
332            amount_out: SwapAmountType::Set(U256::from(20)),
333            ..Default::default()
334        };
335        ready_requests.push(SwapComposeData { swap: Swap::BackrunSwapLine(sp1), ..SwapComposeData::default() });
336
337        let sp2 = SwapLine {
338            path: SwapPath {
339                tokens: vec![token.clone(), token.clone()],
340                pools: vec![],
341                disabled: false,
342                disabled_pool: Default::default(),
343                score: None,
344            },
345            amount_in: SwapAmountType::Set(U256::from(3)),
346            amount_out: SwapAmountType::Set(U256::from(5)),
347            ..Default::default()
348        };
349        ready_requests.push(SwapComposeData { swap: Swap::BackrunSwapLine(sp2), ..SwapComposeData::default() });
350
351        ready_requests.sort_by(|a, b| a.swap.arb_profit().cmp(&b.swap.arb_profit()));
352
353        assert_eq!(ready_requests[0].swap.arb_profit(), U256::from(1));
354        assert_eq!(ready_requests[1].swap.arb_profit(), U256::from(2));
355        assert_eq!(ready_requests[2].swap.arb_profit(), U256::from(10));
356    }
357}