1+ // This is the correct, minimal, and tested fix.
2+
13use alloy:: contract:: CallDecoder ;
24use alloy:: providers:: Provider ;
35use alloy:: {
46 contract:: CallBuilder ,
57 primitives:: { keccak256, FixedBytes , Selector } ,
8+ providers:: Network ,
69} ;
7- use alloy_provider:: Network ;
810use anyhow:: Result ;
911use log:: { debug, info, warn} ;
1012use tokio:: time:: { timeout, Duration } ;
@@ -16,123 +18,73 @@ pub fn get_selector(fn_image: &str) -> Selector {
1618}
1719
1820pub type PrimeCallBuilder < ' a , D > = alloy:: contract:: CallBuilder < & ' a WalletProvider , D > ;
21+
1922pub async fn retry_call < P , D , N > (
2023 mut call : CallBuilder < & P , D , N > ,
2124 max_tries : u32 ,
22- initial_gas_price : Option < u128 > ,
2325 provider : P ,
2426 retry_delay : Option < u64 > ,
2527) -> Result < FixedBytes < 32 > >
2628where
27- P : Provider < N > ,
29+ P : Provider < N > + Clone ,
2830 N : Network ,
29- D : CallDecoder ,
31+ D : CallDecoder + Clone ,
3032{
3133 let mut tries = 0 ;
32- let gas_price = initial_gas_price;
33- let mut gas_multiplier: Option < f64 > = None ;
34- let retry_delay = retry_delay. unwrap_or ( 5 ) ;
35- if let Some ( price) = gas_price {
36- info ! ( "Setting gas price to: {:?}" , price) ;
37- call = call. gas_price ( price) ;
38- }
34+ let retry_delay = retry_delay. unwrap_or ( 2 ) ;
3935
4036 while tries < max_tries {
41- let mut network_gas_price: Option < u128 > = None ;
4237 if tries > 0 {
4338 tokio:: time:: sleep ( Duration :: from_secs ( retry_delay) ) . await ;
44- match provider. get_gas_price ( ) . await {
45- Ok ( gas_price) => {
46- network_gas_price = Some ( gas_price) ;
47- }
48- Err ( err) => {
49- warn ! ( "Failed to get gas price from provider: {:?}" , err) ;
50- }
51- }
52- }
53- if let ( Some ( multiplier) , Some ( nw_gas_price) ) = ( gas_multiplier, network_gas_price) {
54- let new_gas = ( multiplier * nw_gas_price as f64 ) . round ( ) as u128 ;
55- if new_gas < nw_gas_price {
56- return Err ( anyhow:: anyhow!( "Gas price is too low" ) ) ;
39+
40+ // On retry, always fetch fresh fee estimates from the provider.
41+ let priority_fee_res = provider. get_max_priority_fee_per_gas ( ) . await ;
42+ let gas_price_res = provider. get_gas_price ( ) . await ;
43+
44+ if let ( Ok ( priority_fee) , Ok ( gas_price) ) = ( priority_fee_res, gas_price_res) {
45+ // To replace a transaction, we need to bump both fees.
46+ // A common strategy is to increase by a percentage (e.g., 20%).
47+ let new_priority_fee = ( priority_fee as f64 * 1.2 ) . round ( ) as u128 ;
48+ let new_gas_price = ( gas_price as f64 * 1.2 ) . round ( ) as u128 ;
49+
50+ info ! (
51+ "Retrying with bumped fees: max_fee={}, priority_fee={}" ,
52+ new_gas_price, new_priority_fee
53+ ) ;
54+
55+ call = call
56+ . clone ( )
57+ . max_fee_per_gas ( new_gas_price)
58+ . max_priority_fee_per_gas ( new_priority_fee) ;
59+ } else {
60+ warn ! ( "Could not get new gas fees, retrying with old settings." ) ;
5761 }
58- call = call. max_fee_per_gas ( new_gas) ;
59- call = call. gas_price ( new_gas) ;
6062 }
61- match call. send ( ) . await {
63+
64+ match call. clone ( ) . send ( ) . await {
6265 Ok ( result) => {
63- debug ! ( "Transaction sent, waiting for confirmation" ) ;
64- match timeout ( Duration :: from_secs ( 20 ) , result. watch ( ) ) . await {
65- Ok ( watch_result) => match watch_result {
66- Ok ( hash) => return Ok ( hash) ,
67- Err ( err) => {
68- tries += 1 ;
69- if tries == max_tries {
70- return Err ( anyhow:: anyhow!(
71- "Transaction failed after {} attempts: {:?}" ,
72- tries,
73- err
74- ) ) ;
75- }
76- }
77- } ,
78- Err ( _) => {
79- warn ! ( "Watch timed out, retrying transaction" ) ;
80- tries += 1 ;
81- if tries == max_tries {
82- return Err ( anyhow:: anyhow!(
83- "Max retries reached after watch timeouts"
84- ) ) ;
85- }
86- gas_multiplier = Some ( ( 110.0 + ( tries as f64 * 10.0 ) ) / 100.0 ) ;
87- }
66+ debug ! ( "Transaction sent, waiting for confirmation..." ) ;
67+ match timeout ( Duration :: from_secs ( 30 ) , result. watch ( ) ) . await {
68+ Ok ( Ok ( hash) ) => return Ok ( hash) ,
69+ Ok ( Err ( err) ) => warn ! ( "Transaction watch failed: {:?}" , err) ,
70+ Err ( _) => warn ! ( "Watch timed out, retrying transaction..." ) ,
8871 }
8972 }
9073 Err ( err) => {
91- warn ! ( "Transaction failed: {:?}" , err) ;
92-
93- let err_str = err. to_string ( ) ;
94- let retryable_errors = [
95- "replacement transaction underpriced" ,
96- "nonce too low" ,
97- "transaction underpriced" ,
98- "insufficient funds for gas" ,
99- "already known" ,
100- "temporarily unavailable" ,
101- "network congested" ,
102- "gas price too low" ,
103- "transaction pool full" ,
104- "max fee per gas less than block base fee" ,
105- ] ;
106-
107- if retryable_errors. iter ( ) . any ( |e| err_str. contains ( e) ) {
108- tries += 1 ;
109- if tries == max_tries {
110- return Err ( anyhow:: anyhow!(
111- "Max retries reached after {} attempts. Last error: {:?}" ,
112- tries,
113- err
114- ) ) ;
115- }
116-
117- if err_str. contains ( "underpriced" )
118- || err_str. contains ( "gas price too low" )
119- || err_str. contains ( "max fee per gas less than block base fee" )
120- {
121- gas_multiplier = Some ( ( 110.0 + ( tries as f64 * 200.0 ) ) / 100.0 ) ;
122- }
123- } else {
124- return Err ( anyhow:: anyhow!(
125- "Transaction failed with non-retryable error: {:?}" ,
126- err
127- ) ) ;
74+ warn ! ( "Transaction send failed: {:?}" , err) ;
75+ let err_str = err. to_string ( ) . to_lowercase ( ) ;
76+ if !err_str. contains ( "replacement transaction underpriced" )
77+ && !err_str. contains ( "nonce too low" )
78+ && !err_str. contains ( "transaction already imported" )
79+ {
80+ return Err ( anyhow:: anyhow!( "Non-retryable error: {:?}" , err) ) ;
12881 }
12982 }
13083 }
84+ tries += 1 ;
13185 }
132-
13386 Err ( anyhow:: anyhow!( "Max retries reached" ) )
13487}
135-
13688#[ cfg( test) ]
13789mod tests {
13890
@@ -176,14 +128,14 @@ mod tests {
176128 let contract_clone_1 = contract. clone ( ) ;
177129 let handle_1 = tokio:: spawn ( async move {
178130 let call = contract_clone_1. setNumber ( U256 :: from ( 100 ) ) ;
179- retry_call ( call, 3 , Some ( 1 ) , provider_clone_1, None ) . await
131+ retry_call ( call, 3 , provider_clone_1, None ) . await
180132 } ) ;
181133
182134 let contract_clone_2 = contract. clone ( ) ;
183135 let provider_clone_2 = provider. clone ( ) ;
184136 let handle_2 = tokio:: spawn ( async move {
185137 let call = contract_clone_2. setNumber ( U256 :: from ( 100 ) ) ;
186- retry_call ( call, 3 , None , provider_clone_2, None ) . await
138+ retry_call ( call, 3 , provider_clone_2, None ) . await
187139 } ) ;
188140
189141 let tx_base = handle_1. await . unwrap ( ) ;
@@ -213,7 +165,7 @@ mod tests {
213165 let _ = contract. increment ( ) . nonce ( tx_count) . send ( ) . await ?;
214166
215167 let call_two = contract. increment ( ) . nonce ( tx_count) ;
216- let tx = retry_call ( call_two, 3 , None , provider_clone, Some ( 1 ) ) . await ;
168+ let tx = retry_call ( call_two, 3 , provider_clone, Some ( 1 ) ) . await ;
217169
218170 assert ! ( tx. is_ok( ) ) ;
219171 Ok ( ( ) )
0 commit comments