kabu_broadcast_accounts/signers/
signers_component.rs

1use eyre::{Result, eyre};
2use std::sync::Arc;
3use std::time::Duration;
4use tokio::sync::{RwLock, broadcast};
5use tracing::{debug, error, info, warn};
6
7use alloy_network::Network;
8use alloy_primitives::{Address, U256};
9use alloy_provider::Provider;
10
11use kabu_core_components::Component;
12use kabu_types_entities::{AccountNonceAndBalanceState, TxSigners};
13use kabu_types_events::{MessageSwapCompose, SwapComposeMessage};
14use kabu_types_swap::Swap;
15use reth_ethereum_primitives::EthPrimitives;
16use reth_node_types::NodePrimitives;
17use reth_tasks::TaskExecutor;
18use revm::DatabaseRef;
19
20/// Component that handles transaction signing for MEV bot operations
21pub struct SignersComponent<P, N, DB: Send + Sync + Clone + 'static, LDT: NodePrimitives + 'static = EthPrimitives> {
22    /// JSON-RPC provider for chain interactions
23    client: P,
24    /// Transaction signers with private keys
25    signers: Arc<RwLock<TxSigners<LDT>>>,
26    /// Account state for nonce management
27    account_state: Arc<RwLock<AccountNonceAndBalanceState>>,
28    /// Channel to receive swap compose messages
29    swap_compose_rx: Option<broadcast::Receiver<MessageSwapCompose<DB, LDT>>>,
30    /// Channel to send signed transactions
31    signed_tx_tx: Option<broadcast::Sender<MessageSwapCompose<DB, LDT>>>,
32    /// Gas price buffer for dynamic pricing
33    gas_price_buffer: u64,
34    /// Phantom data for network type
35    _network: std::marker::PhantomData<N>,
36}
37
38impl<P, N, DB, LDT> SignersComponent<P, N, DB, LDT>
39where
40    P: Provider<N> + Send + Sync + Clone + 'static,
41    N: Network + 'static,
42    DB: DatabaseRef + Send + Sync + Clone + 'static,
43    LDT: NodePrimitives + 'static,
44{
45    pub fn new(
46        client: P,
47        signers: Arc<RwLock<TxSigners<LDT>>>,
48        account_state: Arc<RwLock<AccountNonceAndBalanceState>>,
49        gas_price_buffer: u64,
50    ) -> Self {
51        Self {
52            client,
53            signers,
54            account_state,
55            swap_compose_rx: None,
56            signed_tx_tx: None,
57            gas_price_buffer,
58            _network: std::marker::PhantomData,
59        }
60    }
61
62    pub fn with_channels(
63        mut self,
64        swap_compose_channel: broadcast::Sender<MessageSwapCompose<DB, LDT>>,
65        signed_tx_channel: broadcast::Sender<MessageSwapCompose<DB, LDT>>,
66    ) -> Self {
67        self.swap_compose_rx = Some(swap_compose_channel.subscribe());
68        self.signed_tx_tx = Some(signed_tx_channel);
69        self
70    }
71
72    async fn run(self) -> Result<()> {
73        info!("Starting signers component");
74
75        // Validate signers are available
76        let signer_count = {
77            let signers = self.signers.read().await;
78            signers.get_address_vec().len()
79        };
80
81        if signer_count == 0 {
82            return Err(eyre!("No signers configured for signing component"));
83        }
84
85        info!("Signers component ready with {} signers", signer_count);
86
87        // Extract receivers and senders
88        let mut swap_compose_rx = self.swap_compose_rx;
89        let signed_tx_tx = self.signed_tx_tx;
90        let signers = self.signers;
91        let account_state = self.account_state;
92        let client = self.client;
93        let gas_price_buffer = self.gas_price_buffer;
94
95        // Main event loop
96        loop {
97            tokio::select! {
98                swap_compose_msg = recv_swap_compose_msg(&mut swap_compose_rx) => {
99                    if let Some(swap_compose) = swap_compose_msg
100                        && let Err(e) = handle_swap_compose_signing(
101                            &client,
102                            &signers,
103                            &account_state,
104                            &signed_tx_tx,
105                            swap_compose,
106                            gas_price_buffer,
107                        ).await {
108                            error!("Error signing swap compose: {}", e);
109                    }
110                }
111                _ = tokio::time::sleep(Duration::from_secs(10)) => {
112                    debug!("Signers component heartbeat");
113                }
114            }
115        }
116    }
117}
118
119/// Standalone helper function to receive swap compose messages
120async fn recv_swap_compose_msg<DB: Send + Sync + Clone + 'static, LDT: NodePrimitives>(
121    swap_compose_rx: &mut Option<broadcast::Receiver<MessageSwapCompose<DB, LDT>>>,
122) -> Option<MessageSwapCompose<DB, LDT>> {
123    if let Some(rx) = swap_compose_rx {
124        match rx.recv().await {
125            Ok(msg) => Some(msg),
126            Err(broadcast::error::RecvError::Lagged(missed)) => {
127                warn!("Signers missed {} swap compose messages", missed);
128                None
129            }
130            Err(broadcast::error::RecvError::Closed) => {
131                error!("Swap compose channel closed");
132                None
133            }
134        }
135    } else {
136        // No swap compose channel, sleep a bit
137        tokio::time::sleep(Duration::from_millis(100)).await;
138        None
139    }
140}
141
142/// Handle signing of a swap compose transaction
143async fn handle_swap_compose_signing<P, N, DB, LDT>(
144    client: &P,
145    signers: &Arc<RwLock<TxSigners<LDT>>>,
146    account_state: &Arc<RwLock<AccountNonceAndBalanceState>>,
147    signed_tx_tx: &Option<broadcast::Sender<MessageSwapCompose<DB, LDT>>>,
148    swap_compose_msg: MessageSwapCompose<DB, LDT>,
149    gas_price_buffer: u64,
150) -> Result<()>
151where
152    P: Provider<N> + Send + Sync + Clone + 'static,
153    N: Network + 'static,
154    DB: DatabaseRef + Send + Sync + Clone + 'static,
155    LDT: NodePrimitives + 'static,
156{
157    // Only process messages that are ready for signing
158    let swap_compose_data = match &swap_compose_msg.inner {
159        SwapComposeMessage::Ready(data) => data,
160        _ => {
161            debug!("Ignoring non-ready swap compose message");
162            return Ok(());
163        }
164    };
165
166    let swap = &swap_compose_data.swap;
167
168    // Select the best signer based on account state and swap requirements
169    let selected_signer = select_signer_for_swap(signers, account_state, swap).await?;
170
171    debug!("Selected signer {} for swap", selected_signer);
172
173    // Get current nonce for the selected signer
174    let current_nonce = {
175        let state = account_state.read().await;
176        state.get_account(&selected_signer).map(|acc| acc.get_nonce()).unwrap_or(0)
177    };
178
179    // Get current gas price and apply buffer
180    let base_gas_price = match client.get_gas_price().await {
181        Ok(price) => price,
182        Err(e) => {
183            warn!("Failed to fetch gas price, using default: {}", e);
184            20_000_000_000u128 // 20 gwei default
185        }
186    };
187
188    let buffered_gas_price = base_gas_price + (base_gas_price * gas_price_buffer as u128 / 100);
189
190    // Create signed transaction data (placeholder - actual signing logic would be more complex)
191    let mut updated_compose_data = swap_compose_data.clone();
192
193    // Update the transaction request with proper nonce and gas price
194    updated_compose_data.tx_compose.nonce = current_nonce;
195    updated_compose_data.tx_compose.priority_gas_fee = buffered_gas_price as u64;
196
197    // Update nonce in account state
198    {
199        let mut state = account_state.write().await;
200        if let Some(account_data) = state.get_mut_account(&selected_signer) {
201            account_data.set_nonce(current_nonce + 1);
202            debug!("Updated nonce for {} to {}", selected_signer, current_nonce + 1);
203        }
204    }
205
206    // Send ready transaction
207    if let Some(tx_sender) = signed_tx_tx {
208        let ready_msg = MessageSwapCompose::ready(updated_compose_data);
209
210        if let Err(e) = tx_sender.send(ready_msg) {
211            warn!("Failed to send ready swap compose: {}", e);
212        } else {
213            debug!("Successfully processed and queued swap compose transaction");
214        }
215    }
216
217    Ok(())
218}
219
220/// Select the most appropriate signer for a given swap
221async fn select_signer_for_swap<LDT: NodePrimitives>(
222    signers: &Arc<RwLock<TxSigners<LDT>>>,
223    account_state: &Arc<RwLock<AccountNonceAndBalanceState>>,
224    _swap: &Swap,
225) -> Result<Address> {
226    let signers_guard = signers.read().await;
227    let available_signers = signers_guard.get_address_vec();
228
229    if available_signers.is_empty() {
230        return Err(eyre!("No signers available"));
231    }
232
233    // For now, use simple round-robin selection
234    // In a more sophisticated implementation, we could consider:
235    // - Account balance requirements
236    // - Nonce gaps
237    // - Gas price optimization
238    // - Transaction urgency
239
240    let state = account_state.read().await;
241
242    // Find signer with sufficient ETH balance
243    for &signer in &available_signers {
244        if let Some(account_data) = state.get_account(&signer) {
245            let eth_balance = account_data.get_eth_balance();
246            // Check if account has sufficient ETH for gas (rough estimate: 0.01 ETH minimum)
247            if eth_balance > U256::from(10_000_000_000_000_000u128) {
248                // 0.01 ETH in wei
249                return Ok(signer);
250            }
251        }
252    }
253
254    // If no signer has sufficient balance, return the first one anyway
255    // (it will fail at execution time with clearer error)
256    Ok(available_signers[0])
257}
258
259impl<P, N, DB, LDT> Component for SignersComponent<P, N, DB, LDT>
260where
261    P: Provider<N> + Send + Sync + Clone + 'static,
262    N: Network + 'static,
263    DB: DatabaseRef + Send + Sync + Clone + 'static,
264    LDT: NodePrimitives + 'static,
265{
266    fn spawn(self, executor: TaskExecutor) -> Result<()> {
267        executor.spawn_critical(self.name(), async move {
268            if let Err(e) = self.run().await {
269                error!("Signers component failed: {}", e);
270            }
271        });
272        Ok(())
273    }
274    fn name(&self) -> &'static str {
275        "SignersComponent"
276    }
277}
278
279/// Builder for SignersComponent
280pub struct SignersComponentBuilder {
281    gas_price_buffer: u64,
282}
283
284impl SignersComponentBuilder {
285    pub fn new() -> Self {
286        Self {
287            gas_price_buffer: 10, // Default 10% gas price buffer
288        }
289    }
290
291    pub fn with_gas_price_buffer(mut self, buffer_percent: u64) -> Self {
292        self.gas_price_buffer = buffer_percent;
293        self
294    }
295
296    pub fn build<P, N, DB, LDT>(
297        self,
298        client: P,
299        signers: Arc<RwLock<TxSigners<LDT>>>,
300        account_state: Arc<RwLock<AccountNonceAndBalanceState>>,
301    ) -> SignersComponent<P, N, DB, LDT>
302    where
303        P: Provider<N> + Send + Sync + Clone + 'static,
304        N: Network + 'static,
305        DB: DatabaseRef + Send + Sync + Clone + 'static,
306        LDT: NodePrimitives + 'static,
307    {
308        SignersComponent::new(client, signers, account_state, self.gas_price_buffer)
309    }
310}
311
312impl Default for SignersComponentBuilder {
313    fn default() -> Self {
314        Self::new()
315    }
316}