openzeppelin_relayer/constants/
evm_transaction.rs

1use crate::models::evm::Speed;
2use chrono::Duration;
3
4pub const DEFAULT_TX_VALID_TIMESPAN: i64 = 8 * 60 * 60 * 1000; // 8 hours in milliseconds
5
6pub const DEFAULT_TRANSACTION_SPEED: Speed = Speed::Fast;
7
8pub const DEFAULT_GAS_LIMIT: u64 = 21000;
9pub const ERC20_TRANSFER_GAS_LIMIT: u64 = 65_000;
10pub const ERC721_TRANSFER_GAS_LIMIT: u64 = 80_000;
11pub const COMPLEX_GAS_LIMIT: u64 = 200_000;
12pub const GAS_TX_CREATE_CONTRACT: u64 = 53000;
13
14pub const GAS_TX_DATA_ZERO: u64 = 4; // Cost per zero byte in data
15pub const GAS_TX_DATA_NONZERO: u64 = 16; // Cost per non-zero byte in data
16
17/// Gas limit buffer multiplier for automatic gas limit estimation, 10% increase
18pub const GAS_LIMIT_BUFFER_MULTIPLIER: u64 = 110;
19
20/// Minimum gas price bump factor for transaction replacements (10% increase)
21pub const MIN_BUMP_FACTOR: f64 = 1.1;
22
23// Maximum number of transaction attempts before considering a NOOP
24pub const MAXIMUM_TX_ATTEMPTS: usize = 50;
25// Maximum number of NOOP transactions to attempt
26pub const MAXIMUM_NOOP_RETRY_ATTEMPTS: u32 = 50;
27
28/// Time to resubmit for Arbitrum networks
29pub const ARBITRUM_TIME_TO_RESUBMIT: i64 = 20_000;
30
31// Gas limit for Arbitrum networks (mainly used for NOOP transactions (with no data), covers L1 + L2 costs)
32pub const ARBITRUM_GAS_LIMIT: u64 = 50_000;
33
34/// Gas price cache refresh timeout in seconds (5 minutes)
35/// Used to cleanup stuck refresh operations that may have failed to complete
36pub const GAS_PRICE_CACHE_REFRESH_TIMEOUT_SECS: u64 = 300;
37
38/// Number of historical blocks to fetch for fee history analysis
39pub const HISTORICAL_BLOCKS: u64 = 4;
40
41// EVM Status check and timeout constants
42
43/// Initial delay before first status check (in seconds)
44pub const EVM_STATUS_CHECK_INITIAL_DELAY_SECONDS: i64 = 8;
45
46/// Minimum age of transaction before allowing resubmission and timeout checks (in seconds)
47/// Transactions younger than this will still get status updates from blockchain,
48/// but resubmission logic and timeout checks are deferred to prevent premature actions.
49pub const EVM_MIN_AGE_FOR_RESUBMIT_SECONDS: i64 = 20;
50
51/// Timeout for preparation phase: Pending → Sent (in minutes)
52/// Increased from 1 to 2 minutes to provide wider recovery window
53pub const EVM_PREPARE_TIMEOUT_MINUTES: i64 = 2;
54
55/// Timeout for submission phase: Sent → Submitted (in minutes)
56pub const EVM_SUBMIT_TIMEOUT_MINUTES: i64 = 5;
57
58/// Timeout for resend phase: Sent → Submitted (in seconds)
59pub const EVM_RESEND_TIMEOUT_SECONDS: i64 = 25;
60
61/// Trigger recovery for stuck Pending transactions (in seconds)
62pub const EVM_PENDING_RECOVERY_TRIGGER_SECONDS: i64 = 20;
63
64/// Minimum age before attempting hash recovery for transactions (in minutes)
65pub const EVM_MIN_AGE_FOR_HASH_RECOVERY_MINUTES: i64 = 2;
66
67/// Minimum number of hashes required before attempting hash recovery
68pub const EVM_MIN_HASHES_FOR_RECOVERY: usize = 3;
69
70/// Get preparation timeout duration
71pub fn get_evm_prepare_timeout() -> Duration {
72    Duration::minutes(EVM_PREPARE_TIMEOUT_MINUTES)
73}
74
75/// Get submission timeout duration
76pub fn get_evm_submit_timeout() -> Duration {
77    Duration::minutes(EVM_SUBMIT_TIMEOUT_MINUTES)
78}
79
80/// Get resend timeout duration
81pub fn get_evm_resend_timeout() -> Duration {
82    Duration::seconds(EVM_RESEND_TIMEOUT_SECONDS)
83}
84
85/// Get pending recovery trigger duration
86pub fn get_evm_pending_recovery_trigger_timeout() -> Duration {
87    Duration::seconds(EVM_PENDING_RECOVERY_TRIGGER_SECONDS)
88}
89
90/// Get status check initial delay duration
91pub fn get_evm_status_check_initial_delay() -> Duration {
92    Duration::seconds(EVM_STATUS_CHECK_INITIAL_DELAY_SECONDS)
93}
94
95/// Get minimum age for hash recovery duration
96pub fn get_evm_min_age_for_hash_recovery() -> Duration {
97    Duration::minutes(EVM_MIN_AGE_FOR_HASH_RECOVERY_MINUTES)
98}
99
100/// Error message patterns indicating a transaction was already submitted or its nonce consumed.
101/// Shared between the retry layer (`is_non_retriable_transaction_rpc_message`) and
102/// the domain layer (`is_already_submitted_error`).
103///
104/// Each entry is a lowercased substring to match against the RPC error message.
105pub const ALREADY_SUBMITTED_PATTERNS: &[&str] = &[
106    "nonce too low",
107    "nonce is too low",
108    "already known",
109    "replacement transaction underpriced",
110    "same hash was already imported",
111];
112
113/// Error message patterns indicating the transaction nonce is ahead of the expected on-chain nonce.
114/// This can be transient (burst ordering: tx N+1 arrives before N) or persistent (counter drift).
115///
116/// Checked **after** `ALREADY_SUBMITTED_PATTERNS` in `classify_submission_error` to avoid
117/// ambiguity. Each entry is a lowercased substring to match against the RPC error message.
118pub const NONCE_TOO_HIGH_PATTERNS: &[&str] = &[
119    "nonce too high",              // Geth, Erigon, Hardhat, Anvil
120    "nonce is too high",           // Geth, Erigon, Hardhat, Anvil
121    "nonce too far in the future", // Besu
122    "exceeds next nonce",          // Nethermind
123    "nonce out of range",          // Arbitrum, Optimism, specialized RPCs
124    "tx-nonce-too-high",           // Certain SaaS RPC providers (e.g. Alchemy/Infura internal)
125];
126
127/// Maximum number of "nonce too high" retries before escalating to a nonce health job.
128/// With ~25s between retries (driven by status checker resend timeout), this means
129/// escalation happens within ~75s — enough time for transient burst ordering to resolve.
130pub const MAX_NONCE_TOO_HIGH_RETRIES: u32 = 3;
131
132/// Maximum number of nonces to scan when detecting gaps between on-chain and local counter.
133/// Gaps beyond this range are logged for operator investigation rather than automated recovery.
134pub const MAX_GAP_SCAN_RANGE: u64 = 100;
135
136/// Metadata key used in `RelayerHealthCheck` to indicate a targeted health action.
137pub const HEALTH_CHECK_ACTION_KEY: &str = "health_check_action";
138
139/// Value for `HEALTH_CHECK_ACTION_KEY` that triggers nonce gap detection and resolution.
140pub const HEALTH_CHECK_ACTION_NONCE_HEALTH: &str = "nonce_health";
141
142/// Optional metadata key carrying a nonce hint for the health action.
143/// When present, `resolve_nonce_gaps` ensures the counter covers at least `hint + 1`
144/// so the scan range includes the hinted nonce. This handles the case where the
145/// counter was reset (e.g., after a restart) but a tx at a higher nonce still exists.
146pub const HEALTH_CHECK_NONCE_HINT_KEY: &str = "nonce_hint";
147/// Checks if a lowercased message matches "known transaction" without matching
148/// "unknown transaction" (substring false positive).
149pub fn matches_known_transaction(msg_lower: &str) -> bool {
150    if let Some(pos) = msg_lower.find("known transaction") {
151        // Reject if preceded by "un" (i.e. "unknown transaction")
152        if msg_lower[..pos].ends_with("un") {
153            return false;
154        }
155        return true;
156    }
157    false
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_nonce_too_high_patterns_match_expected_strings() {
166        let cases = [
167            "nonce too high",
168            "nonce is too high",
169            "nonce too far in the future",
170            "exceeds next nonce",
171            "nonce out of range",
172        ];
173        for case in &cases {
174            let msg_lower = case.to_lowercase();
175            assert!(
176                NONCE_TOO_HIGH_PATTERNS
177                    .iter()
178                    .any(|p| msg_lower.contains(p)),
179                "Expected NONCE_TOO_HIGH_PATTERNS to match: {case}"
180            );
181        }
182    }
183
184    #[test]
185    fn test_matches_known_transaction_does_not_match_nonce_too_high() {
186        let nonce_too_high_msgs = [
187            "nonce too high",
188            "nonce is too high",
189            "nonce too far in the future",
190            "exceeds next nonce",
191            "nonce out of range",
192        ];
193        for msg in &nonce_too_high_msgs {
194            assert!(
195                !matches_known_transaction(&msg.to_lowercase()),
196                "matches_known_transaction should NOT match nonce-too-high message: {msg}"
197            );
198        }
199    }
200
201    #[test]
202    fn test_matches_known_transaction_matches_known_transaction() {
203        assert!(matches_known_transaction("known transaction"));
204        assert!(matches_known_transaction("already known transaction here"));
205    }
206
207    #[test]
208    fn test_matches_known_transaction_does_not_match_unknown_transaction() {
209        assert!(!matches_known_transaction("unknown transaction"));
210        assert!(!matches_known_transaction("unknown transaction status"));
211    }
212}