1use crate::{wallet::Wallet, Error, ProviderType};
10use alloy::network::TransactionBuilder;
11use alloy::primitives::{Address as EthAddress, Bytes, B256, U256};
12use alloy::providers::Provider;
13use alloy::rpc::types::{Block, BlockNumberOrTag, TransactionReceipt, TransactionRequest};
14use std::time::Duration;
15
16#[derive(Debug, Clone)]
18pub struct GasConfig {
19 pub gas_limit_multiplier: f64,
21 pub max_priority_fee_per_gas: Option<U256>,
23 pub max_fee_per_gas: Option<U256>,
25 pub gas_price: Option<U256>,
27}
28
29impl Default for GasConfig {
30 fn default() -> Self {
31 Self {
32 gas_limit_multiplier: 1.2,
33 max_priority_fee_per_gas: None,
34 max_fee_per_gas: None,
35 gas_price: None,
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct RetryConfig {
43 pub max_retries: u32,
45 pub initial_backoff_ms: u64,
47 pub max_backoff_ms: u64,
49 pub backoff_multiplier: f64,
51 pub use_jitter: bool,
53}
54
55impl Default for RetryConfig {
56 fn default() -> Self {
57 Self {
58 max_retries: 3,
59 initial_backoff_ms: 1000,
60 max_backoff_ms: 30000,
61 backoff_multiplier: 2.0,
62 use_jitter: true,
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct GasEstimate {
70 pub gas_limit: U256,
72 pub gas_price: U256,
74 pub base_fee_per_gas: Option<U256>,
76 pub max_priority_fee_per_gas: Option<U256>,
78 pub is_eip1559: bool,
80 pub total_cost: U256,
82}
83
84impl GasEstimate {
85 pub fn gas_price_gwei(&self) -> String {
87 format_gwei(self.gas_price)
88 }
89
90 pub fn base_fee_gwei(&self) -> Option<String> {
92 self.base_fee_per_gas.map(format_gwei)
93 }
94
95 pub fn priority_fee_gwei(&self) -> Option<String> {
97 self.max_priority_fee_per_gas.map(format_gwei)
98 }
99
100 pub fn total_cost_eth(&self) -> String {
102 format_eth(self.total_cost)
103 }
104}
105
106pub struct TransactionExecutor {
108 provider: ProviderType,
109 gas_config: GasConfig,
110 retry_config: RetryConfig,
111}
112
113impl TransactionExecutor {
114 pub fn new(provider: ProviderType) -> Self {
116 Self {
117 provider,
118 gas_config: GasConfig::default(),
119 retry_config: RetryConfig::default(),
120 }
121 }
122
123 pub fn with_gas_config(mut self, config: GasConfig) -> Self {
125 self.gas_config = config;
126 self
127 }
128
129 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
131 self.retry_config = config;
132 self
133 }
134
135 pub async fn estimate_gas(
139 &self,
140 from: EthAddress,
141 to: Option<EthAddress>,
142 value: Option<U256>,
143 data: Option<Vec<u8>>,
144 ) -> Result<GasEstimate, Error> {
145 tracing::debug!("Estimating gas for transaction");
146
147 let mut tx = TransactionRequest::default()
149 .from(from)
150 .value(value.unwrap_or(U256::ZERO));
151
152 if let Some(to_addr) = to {
153 tx = tx.to(to_addr);
154 }
155
156 if let Some(tx_data) = data {
157 tx = tx.input(tx_data.into());
158 }
159
160 let estimated_gas = self.estimate_gas_limit(&tx).await?;
162
163 let gas_limit = U256::from(
165 (estimated_gas.to::<u128>() as f64 * self.gas_config.gas_limit_multiplier) as u128,
166 );
167
168 tracing::debug!(
169 "Estimated gas limit: {} (with {}% buffer)",
170 gas_limit,
171 (self.gas_config.gas_limit_multiplier - 1.0) * 100.0
172 );
173
174 let (gas_price, base_fee, priority_fee, is_eip1559) = self.estimate_gas_price().await?;
176
177 let total_cost = gas_limit * gas_price;
179
180 Ok(GasEstimate {
181 gas_limit,
182 gas_price,
183 base_fee_per_gas: base_fee,
184 max_priority_fee_per_gas: priority_fee,
185 is_eip1559,
186 total_cost,
187 })
188 }
189
190 async fn estimate_gas_limit(&self, tx: &TransactionRequest) -> Result<U256, Error> {
192 let gas = self
193 .provider
194 .inner
195 .estimate_gas(tx.clone())
196 .await
197 .map_err(|e| Error::Transaction(format!("Gas estimation failed: {}", e)))?;
198
199 Ok(U256::from(gas))
200 }
201
202 async fn estimate_gas_price(&self) -> Result<(U256, Option<U256>, Option<U256>, bool), Error> {
204 match self.get_eip1559_fees().await {
206 Ok((base_fee, priority_fee)) => {
207 let max_fee = base_fee * U256::from(2) + priority_fee;
208 tracing::debug!(
209 "Using EIP-1559: base={} gwei, priority={} gwei, max={} gwei",
210 format_gwei(base_fee),
211 format_gwei(priority_fee),
212 format_gwei(max_fee)
213 );
214 Ok((max_fee, Some(base_fee), Some(priority_fee), true))
215 }
216 Err(_) => {
217 let gas_price = self.get_legacy_gas_price().await?;
219 tracing::debug!("Using legacy gas price: {} gwei", format_gwei(gas_price));
220 Ok((gas_price, None, None, false))
221 }
222 }
223 }
224
225 async fn get_eip1559_fees(&self) -> Result<(U256, U256), Error> {
227 let block: Block = self
229 .provider
230 .inner
231 .get_block_by_number(BlockNumberOrTag::Latest)
232 .await
233 .map_err(|e| Error::Connection(format!("Failed to get block: {}", e)))?
234 .ok_or_else(|| Error::Connection("No latest block".to_string()))?;
235
236 let base_fee = block
237 .header
238 .base_fee_per_gas
239 .map(U256::from)
240 .ok_or_else(|| Error::Other("EIP-1559 not supported".to_string()))?;
241
242 let priority_fee = self
244 .gas_config
245 .max_priority_fee_per_gas
246 .unwrap_or_else(|| U256::from(2_000_000_000u64)); Ok((base_fee, priority_fee))
249 }
250
251 async fn get_legacy_gas_price(&self) -> Result<U256, Error> {
253 if let Some(price) = self.gas_config.gas_price {
254 return Ok(price);
255 }
256
257 let gas_price = self
258 .provider
259 .inner
260 .get_gas_price()
261 .await
262 .map_err(|e| Error::Connection(format!("Failed to get gas price: {}", e)))?;
263
264 Ok(U256::from(gas_price))
265 }
266
267 pub async fn build_transaction(
269 &self,
270 wallet: &Wallet,
271 to: EthAddress,
272 value: U256,
273 data: Option<Vec<u8>>,
274 gas_estimate: Option<GasEstimate>,
275 ) -> Result<TransactionRequest, Error> {
276 let from = wallet.eth_address();
277
278 let gas_est = if let Some(est) = gas_estimate {
280 est
281 } else {
282 self.estimate_gas(from, Some(to), Some(value), data.clone())
283 .await?
284 };
285
286 let nonce = self.get_transaction_count(from).await?;
288
289 let mut tx = TransactionRequest::default()
291 .with_from(from)
292 .with_to(to)
293 .with_value(value)
294 .with_gas_limit(gas_est.gas_limit.to::<u64>())
295 .with_nonce(nonce.to::<u64>());
296
297 if gas_est.is_eip1559 {
299 if let Some(base_fee) = gas_est.base_fee_per_gas {
300 let max_fee = base_fee * U256::from(2)
301 + gas_est
302 .max_priority_fee_per_gas
303 .unwrap_or_else(|| U256::from(2_000_000_000u64));
304 tx = tx.with_max_fee_per_gas(max_fee.to::<u128>());
305 }
306
307 if let Some(priority_fee) = gas_est.max_priority_fee_per_gas {
308 tx = tx.with_max_priority_fee_per_gas(priority_fee.to::<u128>());
309 }
310 } else {
311 tx = tx.with_gas_price(gas_est.gas_price.to::<u128>());
312 }
313
314 if let Some(tx_data) = data {
316 tx = tx.with_input(Bytes::from(tx_data));
317 }
318
319 if let Some(chain_id) = wallet.chain_id() {
321 tx = tx.with_chain_id(chain_id);
322 }
323
324 Ok(tx)
325 }
326
327 async fn get_transaction_count(&self, address: EthAddress) -> Result<U256, Error> {
329 let nonce = self
330 .provider
331 .inner
332 .get_transaction_count(address)
333 .await
334 .map_err(|e| Error::Connection(format!("Failed to get nonce: {}", e)))?;
335
336 Ok(U256::from(nonce))
337 }
338
339 pub async fn send_transaction(
341 &self,
342 wallet: &Wallet,
343 to: EthAddress,
344 value: U256,
345 data: Option<Vec<u8>>,
346 ) -> Result<B256, Error> {
347 let tx = self
348 .build_transaction(wallet, to, value, data, None)
349 .await?;
350
351 self.send_raw_transaction(wallet, tx).await
352 }
353
354 pub async fn send_raw_transaction(
356 &self,
357 wallet: &Wallet,
358 tx: TransactionRequest,
359 ) -> Result<B256, Error> {
360 let mut attempts = 0;
361 let mut backoff = Duration::from_millis(self.retry_config.initial_backoff_ms);
362
363 loop {
364 match self.try_send_transaction(wallet, &tx).await {
365 Ok(tx_hash) => {
366 tracing::info!("Transaction sent successfully: {:?}", tx_hash);
367 return Ok(tx_hash);
368 }
369 Err(e) if attempts < self.retry_config.max_retries => {
370 attempts += 1;
371 tracing::warn!(
372 "Transaction failed (attempt {}/{}): {}",
373 attempts,
374 self.retry_config.max_retries,
375 e
376 );
377
378 let delay = if self.retry_config.use_jitter {
380 let jitter =
381 (rand::random::<f64>() * 0.3 + 0.85) * backoff.as_millis() as f64;
382 Duration::from_millis(jitter as u64)
383 } else {
384 backoff
385 };
386
387 tokio::time::sleep(delay).await;
388
389 backoff = Duration::from_millis(std::cmp::min(
391 (backoff.as_millis() as f64 * self.retry_config.backoff_multiplier) as u64,
392 self.retry_config.max_backoff_ms,
393 ));
394 }
395 Err(e) => {
396 tracing::error!("Transaction failed after {} attempts: {}", attempts, e);
397 return Err(e);
398 }
399 }
400 }
401 }
402
403 async fn try_send_transaction(
405 &self,
406 _wallet: &Wallet,
407 tx: &TransactionRequest,
408 ) -> Result<B256, Error> {
409 let pending_tx = self
413 .provider
414 .inner
415 .send_transaction(tx.clone())
416 .await
417 .map_err(|e| Error::Transaction(format!("Failed to send transaction: {}", e)))?;
418
419 let tx_hash = *pending_tx.tx_hash();
421
422 Ok(tx_hash)
423 }
424
425 pub async fn wait_for_confirmation(
427 &self,
428 tx_hash: B256,
429 _confirmations: usize,
430 ) -> Result<Option<TransactionReceipt>, Error> {
431 tracing::info!("Waiting for transaction confirmation: {:?}", tx_hash);
432
433 let receipt = self
434 .provider
435 .inner
436 .get_transaction_receipt(tx_hash)
437 .await
438 .map_err(|e| Error::Transaction(format!("Failed to get receipt: {}", e)))?;
439
440 if let Some(ref r) = receipt {
441 tracing::info!(
442 "Transaction confirmed in block {}: status={}",
443 r.block_number.unwrap_or_default(),
444 r.status()
445 );
446 }
447
448 Ok(receipt)
449 }
450}
451
452fn format_gwei(wei: U256) -> String {
454 let gwei_divisor = U256::from(1_000_000_000u64);
455 let gwei_whole = wei / gwei_divisor;
456 let remainder = wei % gwei_divisor;
457
458 let formatted = format!("{}.{:09}", gwei_whole, remainder);
460 formatted
461 .trim_end_matches('0')
462 .trim_end_matches('.')
463 .to_string()
464}
465
466fn format_eth(wei: U256) -> String {
468 let eth_divisor = U256::from(10_u64.pow(18));
469 let eth_whole = wei / eth_divisor;
470 let remainder = wei % eth_divisor;
471
472 let formatted = format!("{}.{:018}", eth_whole, remainder);
474 formatted
475 .trim_end_matches('0')
476 .trim_end_matches('.')
477 .to_string()
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_gas_config_default() {
486 let config = GasConfig::default();
487 assert_eq!(config.gas_limit_multiplier, 1.2);
488 }
489
490 #[test]
491 fn test_retry_config_default() {
492 let config = RetryConfig::default();
493 assert_eq!(config.max_retries, 3);
494 assert_eq!(config.initial_backoff_ms, 1000);
495 assert!(config.use_jitter);
496 }
497
498 #[test]
499 fn test_format_gwei() {
500 let wei = U256::from(1_000_000_000u64);
501 assert_eq!(format_gwei(wei), "1");
502
503 let wei = U256::from(2_500_000_000u64);
504 assert_eq!(format_gwei(wei), "2.5");
505
506 let wei = U256::from(2_540_000_000u64);
507 assert_eq!(format_gwei(wei), "2.54");
508 }
509
510 #[test]
511 fn test_format_eth() {
512 let wei = U256::from(10_u64.pow(18));
513 assert_eq!(format_eth(wei), "1");
514
515 let wei = U256::from(5 * 10_u64.pow(17));
516 assert_eq!(format_eth(wei), "0.5");
517
518 let wei = U256::from(123 * 10_u64.pow(16));
519 assert_eq!(format_eth(wei), "1.23");
520 }
521}