4
4
backoff:: ExponentialBackoff ,
5
5
ethabi:: ethereum_types:: U64 ,
6
6
ethers:: {
7
- contract:: ContractCall ,
7
+ contract:: { ContractCall , ContractError } ,
8
8
middleware:: Middleware ,
9
- types:: { TransactionReceipt , U256 } ,
9
+ providers:: ProviderError ,
10
+ types:: { transaction:: eip2718:: TypedTransaction , TransactionReceipt , U256 } ,
11
+ } ,
12
+ std:: {
13
+ fmt:: Display ,
14
+ sync:: { atomic:: AtomicU64 , Arc } ,
10
15
} ,
11
- std:: sync:: { atomic:: AtomicU64 , Arc } ,
12
16
tokio:: time:: { timeout, Duration } ,
13
17
tracing,
14
18
} ;
@@ -151,12 +155,17 @@ pub async fn estimate_tx_cost<T: Middleware + 'static>(
151
155
/// the transaction exceeds this limit, the transaction is not submitted.
152
156
/// Note however that any gas_escalation policy is applied to the estimate, so the actual gas used may exceed the limit.
153
157
/// The transaction is retried until it is confirmed on chain or the maximum number of retries is reached.
158
+ /// You can pass an `error_mapper` function that will be called on each retry with the number of retries and the error.
159
+ /// This lets you customize the backoff behavior based on the error type.
154
160
pub async fn submit_tx_with_backoff < T : Middleware + NonceManaged + ' static > (
155
161
middleware : Arc < T > ,
156
162
call : ContractCall < T , ( ) > ,
157
163
gas_limit : U256 ,
158
164
escalation_policy : EscalationPolicy ,
159
- ) -> Result < SubmitTxResult > {
165
+ error_mapper : Option <
166
+ impl Fn ( u64 , backoff:: Error < SubmitTxError < T > > ) -> backoff:: Error < SubmitTxError < T > > ,
167
+ > ,
168
+ ) -> Result < SubmitTxResult , SubmitTxError < T > > {
160
169
let start_time = std:: time:: Instant :: now ( ) ;
161
170
162
171
tracing:: info!( "Started processing event" ) ;
@@ -176,14 +185,19 @@ pub async fn submit_tx_with_backoff<T: Middleware + NonceManaged + 'static>(
176
185
177
186
let gas_multiplier_pct = escalation_policy. get_gas_multiplier_pct ( num_retries) ;
178
187
let fee_multiplier_pct = escalation_policy. get_fee_multiplier_pct ( num_retries) ;
179
- submit_tx (
188
+ let result = submit_tx (
180
189
middleware. clone ( ) ,
181
190
& call,
182
191
padded_gas_limit,
183
192
gas_multiplier_pct,
184
193
fee_multiplier_pct,
185
194
)
186
- . await
195
+ . await ;
196
+ if let Some ( ref mapper) = error_mapper {
197
+ result. map_err ( |e| mapper ( num_retries, e) )
198
+ } else {
199
+ result
200
+ }
187
201
} ,
188
202
|e, dur| {
189
203
let retry_number = num_retries. load ( std:: sync:: atomic:: Ordering :: Relaxed ) ;
@@ -210,6 +224,51 @@ pub async fn submit_tx_with_backoff<T: Middleware + NonceManaged + 'static>(
210
224
} )
211
225
}
212
226
227
+ pub enum SubmitTxError < T : Middleware + NonceManaged + ' static > {
228
+ GasUsageEstimateError ( ContractError < T > ) ,
229
+ GasLimitExceeded { estimate : U256 , limit : U256 } ,
230
+ GasPriceEstimateError ( <T as Middleware >:: Error ) ,
231
+ SubmissionError ( TypedTransaction , <T as Middleware >:: Error ) ,
232
+ ConfirmationTimeout ( TypedTransaction ) ,
233
+ ConfirmationError ( TypedTransaction , ProviderError ) ,
234
+ ReceiptError ( TypedTransaction , TransactionReceipt ) ,
235
+ }
236
+
237
+ impl < T > Display for SubmitTxError < T >
238
+ where
239
+ T : Middleware + NonceManaged + ' static ,
240
+ {
241
+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
242
+ match self {
243
+ SubmitTxError :: GasUsageEstimateError ( e) => {
244
+ write ! ( f, "Error estimating gas for reveal: {:?}" , e)
245
+ }
246
+ SubmitTxError :: GasLimitExceeded { estimate, limit } => write ! (
247
+ f,
248
+ "Gas estimate for reveal with callback is higher than the gas limit {} > {}" ,
249
+ estimate, limit
250
+ ) ,
251
+ SubmitTxError :: GasPriceEstimateError ( e) => write ! ( f, "Gas price estimate error: {}" , e) ,
252
+ SubmitTxError :: SubmissionError ( tx, e) => write ! (
253
+ f,
254
+ "Error submitting the reveal transaction. Tx:{:?}, Error:{:?}" ,
255
+ tx, e
256
+ ) ,
257
+ SubmitTxError :: ConfirmationTimeout ( tx) => {
258
+ write ! ( f, "Tx stuck in mempool. Resetting nonce. Tx:{:?}" , tx)
259
+ }
260
+ SubmitTxError :: ConfirmationError ( tx, e) => write ! (
261
+ f,
262
+ "Error waiting for transaction receipt. Tx:{:?} Error:{:?}" ,
263
+ tx, e
264
+ ) ,
265
+ SubmitTxError :: ReceiptError ( tx, _) => {
266
+ write ! ( f, "Reveal transaction reverted on-chain. Tx:{:?}" , tx, )
267
+ }
268
+ }
269
+ }
270
+ }
271
+
213
272
/// Submit a transaction to the blockchain. It estimates the gas for the transaction,
214
273
/// pads both the gas and fee estimates using the provided multipliers, and submits the transaction.
215
274
/// It will return a permanent or transient error depending on the error type and whether
@@ -221,24 +280,23 @@ pub async fn submit_tx<T: Middleware + NonceManaged + 'static>(
221
280
// A value of 100 submits the tx with the same gas/fee as the estimate.
222
281
gas_estimate_multiplier_pct : u64 ,
223
282
fee_estimate_multiplier_pct : u64 ,
224
- ) -> Result < TransactionReceipt , backoff:: Error < anyhow :: Error > > {
283
+ ) -> Result < TransactionReceipt , backoff:: Error < SubmitTxError < T > > > {
225
284
let gas_estimate_res = call. estimate_gas ( ) . await ;
226
285
227
286
let gas_estimate = gas_estimate_res. map_err ( |e| {
228
287
// we consider the error transient even if it is a contract revert since
229
288
// it can be because of routing to a lagging RPC node. Retrying such errors will
230
289
// incur a few additional RPC calls, but it is fine.
231
- backoff:: Error :: transient ( anyhow ! ( "Error estimating gas for reveal: {:?}" , e) )
290
+ backoff:: Error :: transient ( SubmitTxError :: GasUsageEstimateError ( e) )
232
291
} ) ?;
233
292
234
293
// The gas limit on the simulated transaction is the maximum expected tx gas estimate,
235
294
// but we are willing to pad the gas a bit to ensure reliable submission.
236
295
if gas_estimate > gas_limit {
237
- return Err ( backoff:: Error :: permanent ( anyhow ! (
238
- "Gas estimate for reveal with callback is higher than the gas limit {} > {}" ,
239
- gas_estimate,
240
- gas_limit
241
- ) ) ) ;
296
+ return Err ( backoff:: Error :: permanent ( SubmitTxError :: GasLimitExceeded {
297
+ estimate : gas_estimate,
298
+ limit : gas_limit,
299
+ } ) ) ;
242
300
}
243
301
244
302
// Pad the gas estimate after checking it against the simulation gas limit.
@@ -247,13 +305,11 @@ pub async fn submit_tx<T: Middleware + NonceManaged + 'static>(
247
305
let call = call. clone ( ) . gas ( gas_estimate) ;
248
306
let mut transaction = call. tx . clone ( ) ;
249
307
250
- // manually fill the tx with the gas info, so we can log the details in case of error
308
+ // manually fill the tx with the gas price info, so we can log the details in case of error
251
309
client
252
310
. fill_transaction ( & mut transaction, None )
253
311
. await
254
- . map_err ( |e| {
255
- backoff:: Error :: transient ( anyhow ! ( "Error filling the reveal transaction: {:?}" , e) )
256
- } ) ?;
312
+ . map_err ( |e| backoff:: Error :: transient ( SubmitTxError :: GasPriceEstimateError ( e) ) ) ?;
257
313
258
314
// Apply the fee escalation policy. Note: the unwrap_or_default should never default as we have a gas oracle
259
315
// in the client that sets the gas price.
@@ -271,11 +327,7 @@ pub async fn submit_tx<T: Middleware + NonceManaged + 'static>(
271
327
. send_transaction ( transaction. clone ( ) , None )
272
328
. await
273
329
. map_err ( |e| {
274
- backoff:: Error :: transient ( anyhow ! (
275
- "Error submitting the reveal transaction. Tx:{:?}, Error:{:?}" ,
276
- transaction,
277
- e
278
- ) )
330
+ backoff:: Error :: transient ( SubmitTxError :: SubmissionError ( transaction. clone ( ) , e) )
279
331
} ) ?;
280
332
281
333
let reset_nonce = || {
@@ -292,34 +344,24 @@ pub async fn submit_tx<T: Middleware + NonceManaged + 'static>(
292
344
// in this case ethers internal polling will not reduce the number of retries
293
345
// and keep retrying indefinitely. So we set a manual timeout here and reset the nonce.
294
346
reset_nonce ( ) ;
295
- backoff:: Error :: transient ( anyhow ! (
296
- "Tx stuck in mempool. Resetting nonce. Tx:{:?}" ,
297
- transaction
298
- ) )
347
+ backoff:: Error :: transient ( SubmitTxError :: ConfirmationTimeout ( transaction. clone ( ) ) )
299
348
} ) ?;
300
349
301
350
let receipt = pending_receipt
302
351
. map_err ( |e| {
303
- backoff:: Error :: transient ( anyhow ! (
304
- "Error waiting for transaction receipt. Tx:{:?} Error:{:?}" ,
305
- transaction,
306
- e
307
- ) )
352
+ backoff:: Error :: transient ( SubmitTxError :: ConfirmationError ( transaction. clone ( ) , e) )
308
353
} ) ?
309
354
. ok_or_else ( || {
310
355
// RPC may not return an error on tx submission if the nonce is too high.
311
356
// But we will never get a receipt. So we reset the nonce manager to get the correct nonce.
312
357
reset_nonce ( ) ;
313
- backoff:: Error :: transient ( anyhow ! (
314
- "Can't verify the reveal, probably dropped from mempool. Resetting nonce. Tx:{:?}" ,
315
- transaction
316
- ) )
358
+ backoff:: Error :: transient ( SubmitTxError :: ConfirmationTimeout ( transaction. clone ( ) ) )
317
359
} ) ?;
318
360
319
361
if receipt. status == Some ( U64 :: from ( 0 ) ) {
320
- return Err ( backoff:: Error :: transient ( anyhow ! (
321
- "Reveal transaction reverted on-chain. Tx:{:?}" ,
322
- transaction
362
+ return Err ( backoff:: Error :: transient ( SubmitTxError :: ReceiptError (
363
+ transaction. clone ( ) ,
364
+ receipt . clone ( ) ,
323
365
) ) ) ;
324
366
}
325
367
0 commit comments