1use crate::{wallet::Wallet, Error, ProviderType};
10use ethers::prelude::*;
11use ethers::types::{
12 transaction::eip2718::TypedTransaction, Address as EthAddress, TransactionReceipt,
13 TransactionRequest, H256, U256,
14};
15use std::time::Duration;
16
17#[derive(Debug, Clone)]
19pub struct GasConfig {
20 pub gas_limit_multiplier: f64,
22 pub max_priority_fee_per_gas: Option<U256>,
24 pub max_fee_per_gas: Option<U256>,
26 pub gas_price: Option<U256>,
28}
29
30impl Default for GasConfig {
31 fn default() -> Self {
32 Self {
33 gas_limit_multiplier: 1.2,
34 max_priority_fee_per_gas: None,
35 max_fee_per_gas: None,
36 gas_price: None,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct RetryConfig {
44 pub max_retries: u32,
46 pub initial_backoff_ms: u64,
48 pub max_backoff_ms: u64,
50 pub backoff_multiplier: f64,
52 pub use_jitter: bool,
54}
55
56impl Default for RetryConfig {
57 fn default() -> Self {
58 Self {
59 max_retries: 3,
60 initial_backoff_ms: 1000,
61 max_backoff_ms: 30000,
62 backoff_multiplier: 2.0,
63 use_jitter: true,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct GasEstimate {
71 pub gas_limit: U256,
73 pub gas_price: U256,
75 pub base_fee_per_gas: Option<U256>,
77 pub max_priority_fee_per_gas: Option<U256>,
79 pub is_eip1559: bool,
81 pub total_cost: U256,
83}
84
85impl GasEstimate {
86 pub fn gas_price_gwei(&self) -> String {
88 format_gwei(self.gas_price)
89 }
90
91 pub fn base_fee_gwei(&self) -> Option<String> {
93 self.base_fee_per_gas.map(format_gwei)
94 }
95
96 pub fn priority_fee_gwei(&self) -> Option<String> {
98 self.max_priority_fee_per_gas.map(format_gwei)
99 }
100
101 pub fn total_cost_eth(&self) -> String {
103 format_eth(self.total_cost)
104 }
105}
106
107pub struct TransactionExecutor {
109 provider: ProviderType,
110 gas_config: GasConfig,
111 retry_config: RetryConfig,
112}
113
114impl TransactionExecutor {
115 pub fn new(provider: ProviderType) -> Self {
117 Self {
118 provider,
119 gas_config: GasConfig::default(),
120 retry_config: RetryConfig::default(),
121 }
122 }
123
124 pub fn with_gas_config(mut self, config: GasConfig) -> Self {
126 self.gas_config = config;
127 self
128 }
129
130 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
132 self.retry_config = config;
133 self
134 }
135
136 pub async fn estimate_gas(
140 &self,
141 from: EthAddress,
142 to: Option<EthAddress>,
143 value: Option<U256>,
144 data: Option<Vec<u8>>,
145 ) -> Result<GasEstimate, Error> {
146 tracing::debug!("Estimating gas for transaction");
147
148 let mut tx = TransactionRequest::new()
150 .from(from)
151 .value(value.unwrap_or(U256::zero()));
152
153 if let Some(to_addr) = to {
154 tx = tx.to(to_addr);
155 }
156
157 if let Some(tx_data) = data {
158 tx = tx.data(tx_data);
159 }
160
161 let estimated_gas = self.estimate_gas_limit(&tx).await?;
163
164 let gas_limit = U256::from(
166 (estimated_gas.as_u128() as f64 * self.gas_config.gas_limit_multiplier) as u128,
167 );
168
169 tracing::debug!(
170 "Estimated gas limit: {} (with {}% buffer)",
171 gas_limit,
172 (self.gas_config.gas_limit_multiplier - 1.0) * 100.0
173 );
174
175 let (gas_price, base_fee, priority_fee, is_eip1559) = self.estimate_gas_price().await?;
177
178 let total_cost = gas_limit * gas_price;
180
181 Ok(GasEstimate {
182 gas_limit,
183 gas_price,
184 base_fee_per_gas: base_fee,
185 max_priority_fee_per_gas: priority_fee,
186 is_eip1559,
187 total_cost,
188 })
189 }
190
191 async fn estimate_gas_limit(&self, tx: &TransactionRequest) -> Result<U256, Error> {
193 let typed_tx: TypedTransaction = tx.clone().into();
194
195 match &self.provider {
196 ProviderType::Http(p) => p
197 .estimate_gas(&typed_tx, None)
198 .await
199 .map_err(|e| Error::Transaction(format!("Gas estimation failed: {}", e))),
200 ProviderType::Ws(p) => p
201 .estimate_gas(&typed_tx, None)
202 .await
203 .map_err(|e| Error::Transaction(format!("Gas estimation failed: {}", e))),
204 }
205 }
206
207 async fn estimate_gas_price(&self) -> Result<(U256, Option<U256>, Option<U256>, bool), Error> {
209 match self.get_eip1559_fees().await {
211 Ok((base_fee, priority_fee)) => {
212 let max_fee = base_fee * 2 + priority_fee;
213 tracing::debug!(
214 "Using EIP-1559: base={} gwei, priority={} gwei, max={} gwei",
215 format_gwei(base_fee),
216 format_gwei(priority_fee),
217 format_gwei(max_fee)
218 );
219 Ok((max_fee, Some(base_fee), Some(priority_fee), true))
220 }
221 Err(_) => {
222 let gas_price = self.get_legacy_gas_price().await?;
224 tracing::debug!("Using legacy gas price: {} gwei", format_gwei(gas_price));
225 Ok((gas_price, None, None, false))
226 }
227 }
228 }
229
230 async fn get_eip1559_fees(&self) -> Result<(U256, U256), Error> {
232 let base_fee = match &self.provider {
234 ProviderType::Http(p) => {
235 let block = p
236 .get_block(BlockNumber::Latest)
237 .await
238 .map_err(|e| Error::Connection(format!("Failed to get block: {}", e)))?
239 .ok_or_else(|| Error::Connection("No latest block".to_string()))?;
240
241 block
242 .base_fee_per_gas
243 .ok_or_else(|| Error::Other("EIP-1559 not supported".to_string()))?
244 }
245 ProviderType::Ws(p) => {
246 let block = p
247 .get_block(BlockNumber::Latest)
248 .await
249 .map_err(|e| Error::Connection(format!("Failed to get block: {}", e)))?
250 .ok_or_else(|| Error::Connection("No latest block".to_string()))?;
251
252 block
253 .base_fee_per_gas
254 .ok_or_else(|| Error::Other("EIP-1559 not supported".to_string()))?
255 }
256 };
257
258 let priority_fee = self
260 .gas_config
261 .max_priority_fee_per_gas
262 .unwrap_or_else(|| U256::from(2_000_000_000u64)); Ok((base_fee, priority_fee))
265 }
266
267 async fn get_legacy_gas_price(&self) -> Result<U256, Error> {
269 if let Some(price) = self.gas_config.gas_price {
270 return Ok(price);
271 }
272
273 match &self.provider {
274 ProviderType::Http(p) => p
275 .get_gas_price()
276 .await
277 .map_err(|e| Error::Connection(format!("Failed to get gas price: {}", e))),
278 ProviderType::Ws(p) => p
279 .get_gas_price()
280 .await
281 .map_err(|e| Error::Connection(format!("Failed to get gas price: {}", e))),
282 }
283 }
284
285 pub async fn build_transaction(
287 &self,
288 wallet: &Wallet,
289 to: EthAddress,
290 value: U256,
291 data: Option<Vec<u8>>,
292 gas_estimate: Option<GasEstimate>,
293 ) -> Result<TypedTransaction, Error> {
294 let from = wallet.eth_address();
295
296 let gas_est = if let Some(est) = gas_estimate {
298 est
299 } else {
300 self.estimate_gas(from, Some(to), Some(value), data.clone())
301 .await?
302 };
303
304 let nonce = self.get_transaction_count(from).await?;
306
307 let mut tx = if gas_est.is_eip1559 {
309 let mut eip1559_tx = Eip1559TransactionRequest::new()
310 .from(from)
311 .to(to)
312 .value(value)
313 .gas(gas_est.gas_limit)
314 .nonce(nonce);
315
316 if let Some(base_fee) = gas_est.base_fee_per_gas {
317 let max_fee = base_fee * 2
318 + gas_est
319 .max_priority_fee_per_gas
320 .unwrap_or_else(|| U256::from(2_000_000_000u64));
321 eip1559_tx = eip1559_tx.max_fee_per_gas(max_fee);
322 }
323
324 if let Some(priority_fee) = gas_est.max_priority_fee_per_gas {
325 eip1559_tx = eip1559_tx.max_priority_fee_per_gas(priority_fee);
326 }
327
328 if let Some(tx_data) = data {
329 eip1559_tx = eip1559_tx.data(tx_data);
330 }
331
332 TypedTransaction::Eip1559(eip1559_tx)
333 } else {
334 let mut legacy_tx = TransactionRequest::new()
335 .from(from)
336 .to(to)
337 .value(value)
338 .gas(gas_est.gas_limit)
339 .gas_price(gas_est.gas_price)
340 .nonce(nonce);
341
342 if let Some(tx_data) = data {
343 legacy_tx = legacy_tx.data(tx_data);
344 }
345
346 TypedTransaction::Legacy(legacy_tx)
347 };
348
349 if let Some(chain_id) = wallet.chain_id() {
351 tx.set_chain_id(chain_id);
352 }
353
354 Ok(tx)
355 }
356
357 async fn get_transaction_count(&self, address: EthAddress) -> Result<U256, Error> {
359 match &self.provider {
360 ProviderType::Http(p) => p
361 .get_transaction_count(address, None)
362 .await
363 .map_err(|e| Error::Connection(format!("Failed to get nonce: {}", e))),
364 ProviderType::Ws(p) => p
365 .get_transaction_count(address, None)
366 .await
367 .map_err(|e| Error::Connection(format!("Failed to get nonce: {}", e))),
368 }
369 }
370
371 pub async fn send_transaction(
373 &self,
374 wallet: &Wallet,
375 to: EthAddress,
376 value: U256,
377 data: Option<Vec<u8>>,
378 ) -> Result<H256, Error> {
379 let tx = self
380 .build_transaction(wallet, to, value, data, None)
381 .await?;
382
383 self.send_raw_transaction(wallet, tx).await
384 }
385
386 pub async fn send_raw_transaction(
388 &self,
389 wallet: &Wallet,
390 tx: TypedTransaction,
391 ) -> Result<H256, Error> {
392 let mut attempts = 0;
393 let mut backoff = Duration::from_millis(self.retry_config.initial_backoff_ms);
394
395 loop {
396 match self.try_send_transaction(wallet, &tx).await {
397 Ok(tx_hash) => {
398 tracing::info!("Transaction sent successfully: {:?}", tx_hash);
399 return Ok(tx_hash);
400 }
401 Err(e) if attempts < self.retry_config.max_retries => {
402 attempts += 1;
403 tracing::warn!(
404 "Transaction failed (attempt {}/{}): {}",
405 attempts,
406 self.retry_config.max_retries,
407 e
408 );
409
410 let delay = if self.retry_config.use_jitter {
412 let jitter =
413 (rand::random::<f64>() * 0.3 + 0.85) * backoff.as_millis() as f64;
414 Duration::from_millis(jitter as u64)
415 } else {
416 backoff
417 };
418
419 tokio::time::sleep(delay).await;
420
421 backoff = Duration::from_millis(std::cmp::min(
423 (backoff.as_millis() as f64 * self.retry_config.backoff_multiplier) as u64,
424 self.retry_config.max_backoff_ms,
425 ));
426 }
427 Err(e) => {
428 tracing::error!("Transaction failed after {} attempts: {}", attempts, e);
429 return Err(e);
430 }
431 }
432 }
433 }
434
435 async fn try_send_transaction(
437 &self,
438 wallet: &Wallet,
439 tx: &TypedTransaction,
440 ) -> Result<H256, Error> {
441 let signature = wallet.sign_transaction(tx).await?;
443
444 let signed_tx = tx.rlp_signed(&signature);
446
447 let tx_hash = match &self.provider {
449 ProviderType::Http(p) => {
450 let pending = p
451 .send_raw_transaction(signed_tx.clone())
452 .await
453 .map_err(|e| {
454 Error::Transaction(format!("Failed to send transaction: {}", e))
455 })?;
456 *pending
457 }
458 ProviderType::Ws(p) => {
459 let pending = p
460 .send_raw_transaction(signed_tx.clone())
461 .await
462 .map_err(|e| {
463 Error::Transaction(format!("Failed to send transaction: {}", e))
464 })?;
465 *pending
466 }
467 };
468
469 Ok(tx_hash)
470 }
471
472 pub async fn wait_for_confirmation(
474 &self,
475 tx_hash: H256,
476 confirmations: usize,
477 ) -> Result<Option<TransactionReceipt>, Error> {
478 tracing::info!(
479 "Waiting for {} confirmations of transaction {:?}",
480 confirmations,
481 tx_hash
482 );
483
484 let receipt = match &self.provider {
485 ProviderType::Http(p) => p
486 .get_transaction_receipt(tx_hash)
487 .await
488 .map_err(|e| Error::Transaction(format!("Failed to get receipt: {}", e)))?,
489 ProviderType::Ws(p) => p
490 .get_transaction_receipt(tx_hash)
491 .await
492 .map_err(|e| Error::Transaction(format!("Failed to get receipt: {}", e)))?,
493 };
494
495 if let Some(ref r) = receipt {
496 tracing::info!(
497 "Transaction confirmed in block {}: status={}",
498 r.block_number.unwrap_or_default(),
499 r.status.unwrap_or_default()
500 );
501 }
502
503 Ok(receipt)
504 }
505}
506
507fn format_gwei(wei: U256) -> String {
509 let gwei_divisor = U256::from(1_000_000_000u64);
510 let gwei_whole = wei / gwei_divisor;
511 let remainder = wei % gwei_divisor;
512
513 let formatted = format!("{}.{:09}", gwei_whole, remainder);
515 formatted
516 .trim_end_matches('0')
517 .trim_end_matches('.')
518 .to_string()
519}
520
521fn format_eth(wei: U256) -> String {
523 let eth_divisor = U256::from(10_u64.pow(18));
524 let eth_whole = wei / eth_divisor;
525 let remainder = wei % eth_divisor;
526
527 let formatted = format!("{}.{:018}", eth_whole, remainder);
529 formatted
530 .trim_end_matches('0')
531 .trim_end_matches('.')
532 .to_string()
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
540 fn test_gas_config_default() {
541 let config = GasConfig::default();
542 assert_eq!(config.gas_limit_multiplier, 1.2);
543 }
544
545 #[test]
546 fn test_retry_config_default() {
547 let config = RetryConfig::default();
548 assert_eq!(config.max_retries, 3);
549 assert_eq!(config.initial_backoff_ms, 1000);
550 assert!(config.use_jitter);
551 }
552
553 #[test]
554 fn test_format_gwei() {
555 let wei = U256::from(1_000_000_000u64);
556 assert_eq!(format_gwei(wei), "1");
557
558 let wei = U256::from(2_500_000_000u64);
559 assert_eq!(format_gwei(wei), "2.5");
560
561 let wei = U256::from(2_540_000_000u64);
562 assert_eq!(format_gwei(wei), "2.54");
563 }
564
565 #[test]
566 fn test_format_eth() {
567 let wei = U256::from(10_u64.pow(18));
568 assert_eq!(format_eth(wei), "1");
569
570 let wei = U256::from(5 * 10_u64.pow(17));
571 assert_eq!(format_eth(wei), "0.5");
572
573 let wei = U256::from(123 * 10_u64.pow(16));
574 assert_eq!(format_eth(wei), "1.23");
575 }
576}