@@ -7,6 +7,7 @@ use tracing::debug;
77
88use crate :: config:: ServerConfig ;
99use crate :: constants:: {
10+ matches_known_transaction, ALREADY_SUBMITTED_PATTERNS ,
1011 DEFAULT_HTTP_CLIENT_CONNECT_TIMEOUT_SECONDS ,
1112 DEFAULT_HTTP_CLIENT_HTTP2_KEEP_ALIVE_INTERVAL_SECONDS ,
1213 DEFAULT_HTTP_CLIENT_HTTP2_KEEP_ALIVE_TIMEOUT_SECONDS ,
@@ -441,6 +442,20 @@ pub fn should_mark_provider_failed(error: &ProviderError) -> bool {
441442 }
442443}
443444
445+ /// Returns true if the RPC error message indicates a transaction-level error
446+ /// that should not be retried — the RPC is working correctly, but rejecting
447+ /// the transaction itself.
448+ ///
449+ /// Uses the shared `ALREADY_SUBMITTED_PATTERNS` from constants, consistent with
450+ /// `is_already_submitted_error` in `domain::transaction::evm::evm_transaction`.
451+ fn is_non_retriable_transaction_rpc_message ( message : & str ) -> bool {
452+ let msg_lower = message. to_lowercase ( ) ;
453+ ALREADY_SUBMITTED_PATTERNS
454+ . iter ( )
455+ . any ( |p| msg_lower. contains ( p) )
456+ || matches_known_transaction ( & msg_lower)
457+ }
458+
444459// Errors that are retriable
445460pub fn is_retriable_error ( error : & ProviderError ) -> bool {
446461 match error {
@@ -470,14 +485,16 @@ pub fn is_retriable_error(error: &ProviderError) -> bool {
470485 }
471486
472487 // JSON-RPC error codes (EIP-1474)
473- ProviderError :: RpcErrorCode { code, .. } => {
488+ ProviderError :: RpcErrorCode { code, message } => {
474489 match code {
475- // -32002: Resource unavailable (temporary state)
476- -32002 => true ,
490+ // -32002: Resource unavailable — retriable unless the message indicates a
491+ // transaction-level rejection (some providers wrap nonce/tx errors here)
492+ -32002 => !is_non_retriable_transaction_rpc_message ( message) ,
477493 // -32005: Limit exceeded / rate limited
478494 -32005 => true ,
479- // -32603: Internal error (may be temporary)
480- -32603 => true ,
495+ // -32603: Internal error — retriable unless the message indicates a
496+ // transaction-level rejection (some providers wrap nonce/tx errors here)
497+ -32603 => !is_non_retriable_transaction_rpc_message ( message) ,
481498 // -32000: Invalid input
482499 -32000 => false ,
483500 // -32001: Resource not found
@@ -1320,4 +1337,85 @@ mod tests {
13201337 ) ;
13211338 }
13221339 }
1340+
1341+ #[ test]
1342+ fn test_is_non_retriable_transaction_rpc_message ( ) {
1343+ // Positive cases: these messages should be recognized as non-retriable
1344+ assert ! ( is_non_retriable_transaction_rpc_message( "nonce too low" ) ) ;
1345+ assert ! ( is_non_retriable_transaction_rpc_message( "Nonce Too Low" ) ) ;
1346+ assert ! ( is_non_retriable_transaction_rpc_message( "nonce is too low" ) ) ;
1347+ assert ! ( is_non_retriable_transaction_rpc_message( "already known" ) ) ;
1348+ assert ! ( is_non_retriable_transaction_rpc_message(
1349+ "known transaction"
1350+ ) ) ;
1351+ assert ! ( is_non_retriable_transaction_rpc_message(
1352+ "Known Transaction"
1353+ ) ) ;
1354+ assert ! ( is_non_retriable_transaction_rpc_message(
1355+ "replacement transaction underpriced"
1356+ ) ) ;
1357+ assert ! ( is_non_retriable_transaction_rpc_message(
1358+ "same hash was already imported"
1359+ ) ) ;
1360+ assert ! ( is_non_retriable_transaction_rpc_message(
1361+ "Transaction nonce too low"
1362+ ) ) ;
1363+
1364+ // Negative cases: generic/unrelated messages should not match
1365+ assert ! ( !is_non_retriable_transaction_rpc_message( "Internal error" ) ) ;
1366+ assert ! ( !is_non_retriable_transaction_rpc_message( "server busy" ) ) ;
1367+ assert ! ( !is_non_retriable_transaction_rpc_message( "" ) ) ;
1368+ // "unknown transaction" must NOT match "known transaction"
1369+ assert ! ( !is_non_retriable_transaction_rpc_message(
1370+ "Unknown transaction status"
1371+ ) ) ;
1372+ }
1373+
1374+ #[ test]
1375+ fn test_is_retriable_error_rpc_tx_errors_not_retriable ( ) {
1376+ // Transaction-level messages that should NOT be retriable regardless of code
1377+ let non_retriable_messages = vec ! [
1378+ "Transaction nonce too low" ,
1379+ "nonce too low" ,
1380+ "nonce is too low" ,
1381+ "already known" ,
1382+ "known transaction" ,
1383+ "replacement transaction underpriced" ,
1384+ "same hash was already imported" ,
1385+ ] ;
1386+
1387+ // Messages that should remain retriable (generic/unrelated)
1388+ let retriable_messages = vec ! [
1389+ "Internal error" ,
1390+ "" ,
1391+ // "unknown transaction" must NOT false-positive on "known transaction"
1392+ "Unknown transaction status" ,
1393+ "Resource unavailable" ,
1394+ ] ;
1395+
1396+ // Both -32603 and -32002 should behave the same way for tx-level messages
1397+ for code in [ -32603 , -32002 ] {
1398+ for message in & non_retriable_messages {
1399+ let error = ProviderError :: RpcErrorCode {
1400+ code,
1401+ message : message. to_string ( ) ,
1402+ } ;
1403+ assert ! (
1404+ !is_retriable_error( & error) ,
1405+ "{code} with message {message:?} should NOT be retriable"
1406+ ) ;
1407+ }
1408+
1409+ for message in & retriable_messages {
1410+ let error = ProviderError :: RpcErrorCode {
1411+ code,
1412+ message : message. to_string ( ) ,
1413+ } ;
1414+ assert ! (
1415+ is_retriable_error( & error) ,
1416+ "{code} with message {message:?} should be retriable"
1417+ ) ;
1418+ }
1419+ }
1420+ }
13231421}
0 commit comments