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}