From 504f6bc710dd73bd7f9b7946a92902e051145088 Mon Sep 17 00:00:00 2001 From: Josh Stevens Date: Sun, 9 Aug 2026 12:45:09 -0400 Subject: [PATCH 1/2] chore: deny panic/expect/unwrap via workspace clippy lints Add workspace-level clippy lints (unwrap_used, expect_used, panic, todo, unimplemented, unreachable) enforced as errors in core, cli and sdk, with test-code exemptions via clippy.toml. Replace all violating call sites with proper error propagation or logged fallbacks. Co-Authored-By: Claude Fable 5 --- Cargo.toml | 8 ++ clippy.toml | 3 + crates/cli/Cargo.toml | 3 + crates/cli/src/commands/network.rs | 11 ++- crates/cli/src/commands/tx.rs | 15 ++- crates/core/Cargo.toml | 3 + crates/core/build.rs | 27 ++++-- crates/core/src/authentication/basic_auth.rs | 6 +- .../src/background_tasks/balance_monitor.rs | 11 ++- .../core/src/gas/fee_estimator/etherscan.rs | 2 +- crates/core/src/gas/fee_estimator/fallback.rs | 28 ++++-- crates/core/src/gas/fee_estimator/tenderly.rs | 16 +-- crates/core/src/provider/evm_provider.rs | 2 +- crates/core/src/rate_limiting/rate_limiter.rs | 11 ++- crates/core/src/relayer/cache.rs | 3 +- crates/core/src/relayer/db/write.rs | 97 ++++++++++++------- crates/core/src/safe_proxy.rs | 2 +- crates/core/src/shared/cache.rs | 52 ++++++---- crates/core/src/startup.rs | 36 +++++-- crates/core/src/transaction/cache.rs | 3 +- .../queue_system/transactions_queues.rs | 38 +++++--- .../core/src/wallet/aws_kms_wallet_manager.rs | 6 +- .../core/src/wallet/turnkey_wallet_manager.rs | 18 ++-- crates/core/src/yaml.rs | 26 ++++- crates/sdk/Cargo.toml | 3 + crates/sdk/src/api/http.rs | 41 ++++---- crates/sdk/src/api/sign/mod.rs | 13 ++- crates/sdk/src/api/transaction/mod.rs | 16 ++- 28 files changed, 337 insertions(+), 163 deletions(-) create mode 100644 clippy.toml diff --git a/Cargo.toml b/Cargo.toml index f6f30469..4559a5b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,14 @@ members = [ "playground/rust-sdk-playground", ] +[workspace.lints.clippy] +unwrap_used = "deny" +expect_used = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +unreachable = "deny" + [workspace.dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1.0" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..7bada547 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,3 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-panic-in-tests = true diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 79badcb7..47160ee9 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -34,3 +34,6 @@ rand = "0.8.5" [features] jemalloc = ["dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"] + +[lints] +workspace = true diff --git a/crates/cli/src/commands/network.rs b/crates/cli/src/commands/network.rs index 1d1a0664..d5f92c84 100644 --- a/crates/cli/src/commands/network.rs +++ b/crates/cli/src/commands/network.rs @@ -94,11 +94,16 @@ async fn handle_add(project_path: &ProjectLocation) -> Result<(), NetworkError> .allow_empty(true) .interact_text()?; + let first_provider_url = provider_urls.first().ok_or_else(|| { + NetworkError::InvalidConfig("At least one provider URL is required".to_string()) + })?; + let chain_id = get_chain_id(first_provider_url).await.map_err(|e| { + NetworkError::ConnectionFailed(format!("Could not read from rpc for the chain id: {}", e)) + })?; + setup_config.networks.push(NetworkSetupConfig { name: network_name.clone(), - chain_id: get_chain_id(provider_urls.first().unwrap()) - .await - .expect("Could not read from rpc for the chain id"), + chain_id, signing_provider: None, provider_urls, block_explorer_url: if block_explorer.is_empty() { None } else { Some(block_explorer) }, diff --git a/crates/cli/src/commands/tx.rs b/crates/cli/src/commands/tx.rs index c55e28c7..a662fc62 100644 --- a/crates/cli/src/commands/tx.rs +++ b/crates/cli/src/commands/tx.rs @@ -395,7 +395,9 @@ async fn handle_fund( .into(); let network = client.network().get_all().await?; - let network = network.into_iter().find(|n| n.chain_id == chain_id).expect("Network not found"); + let network = network.into_iter().find(|n| n.chain_id == chain_id).ok_or_else(|| { + TransactionError::CommandFailed(format!("Network not found: {}", chain_id)) + })?; println!("┌─────────────────────────────────────────────────────────────────────"); println!("│ FUNDING RELAYER"); @@ -445,7 +447,16 @@ async fn handle_fund( let wallet = EthereumWallet::from(signer); - let rpc_url = network.provider_urls.first().expect("Providers not found").to_string(); + let rpc_url = network + .provider_urls + .first() + .ok_or_else(|| { + TransactionError::CommandFailed(format!( + "Providers not found for network {}", + network.name + )) + })? + .to_string(); let provider = ProviderBuilder::new().wallet(wallet).connect(&rpc_url).await.map_err(|e| { TransactionError::CommandFailed(format!("Failed to connect to RPC: {}", e)) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 04ed0d34..eec1ff60 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -68,3 +68,6 @@ rsa = "0.9" [build-dependencies] chrono = "0.4.19" + +[lints] +workspace = true diff --git a/crates/core/build.rs b/crates/core/build.rs index 0c717e4c..3fcf0763 100644 --- a/crates/core/build.rs +++ b/crates/core/build.rs @@ -58,8 +58,11 @@ fn branch_name_from_ref(branch_ref: &str) -> Option<&str> { } } -fn epoch_to_iso_timestamp(epoch: i64) -> String { - Utc.timestamp_opt(epoch, 0).unwrap().to_rfc3339_opts(SecondsFormat::Secs, true) +fn epoch_to_iso_timestamp(epoch: i64) -> Result { + Utc.timestamp_opt(epoch, 0) + .single() + .map(|timestamp| timestamp.to_rfc3339_opts(SecondsFormat::Secs, true)) + .ok_or_else(|| format!("invalid epoch timestamp: {epoch}")) } fn main() -> Result<(), Box> { @@ -79,8 +82,12 @@ fn main() -> Result<(), Box> { set_env_if_needed( "BUILD_COMMIT_TIMESTAMP_ISO", - &command_output("git", &["log", "-1", "--format=%ct"]) - .map(|epoch| epoch_to_iso_timestamp(epoch.parse().unwrap())), + &command_output("git", &["log", "-1", "--format=%ct"]).and_then(|epoch| { + let epoch = epoch + .parse() + .map_err(|e| format!("could not parse commit timestamp `{epoch}`: {e}"))?; + epoch_to_iso_timestamp(epoch) + }), ); // TODO: get commit labels @@ -136,11 +143,13 @@ fn main() -> Result<(), Box> { ); // https://reproducible-builds.org/docs/source-date-epoch/#rust - let build_timestamp_iso = epoch_to_iso_timestamp( - option_env!("SOURCE_DATE_EPOCH") - .map(|epoch| epoch.parse().unwrap()) - .unwrap_or_else(|| Utc::now().timestamp()), - ); + let build_epoch = match option_env!("SOURCE_DATE_EPOCH") { + Some(epoch) => epoch + .parse() + .map_err(|e| format!("could not parse SOURCE_DATE_EPOCH `{epoch}`: {e}"))?, + None => Utc::now().timestamp(), + }; + let build_timestamp_iso = epoch_to_iso_timestamp(build_epoch)?; println!("cargo:rustc-env=BUILD_TIMESTAMP_ISO={build_timestamp_iso}"); Ok(()) diff --git a/crates/core/src/authentication/basic_auth.rs b/crates/core/src/authentication/basic_auth.rs index c9811ae8..d8d70fc6 100644 --- a/crates/core/src/authentication/basic_auth.rs +++ b/crates/core/src/authentication/basic_auth.rs @@ -7,7 +7,7 @@ use axum::response::Response; use axum::{ async_trait, extract::FromRequestParts, - http::{request::Parts, HeaderMap, StatusCode}, + http::{request::Parts, HeaderMap, HeaderValue, StatusCode}, }; use base64::{engine::general_purpose, Engine as _}; use subtle::ConstantTimeEq; @@ -128,9 +128,9 @@ pub async fn inject_basic_auth_status( let basic_auth_valid = validate_basic_auth(req.headers()).is_ok(); if basic_auth_valid { - req.headers_mut().insert("x-rrelayer-basic-auth-valid", "true".parse().unwrap()); + req.headers_mut().insert("x-rrelayer-basic-auth-valid", HeaderValue::from_static("true")); } else { - req.headers_mut().insert("x-rrelayer-basic-auth-valid", "false".parse().unwrap()); + req.headers_mut().insert("x-rrelayer-basic-auth-valid", HeaderValue::from_static("false")); } Ok(next.run(req).await) diff --git a/crates/core/src/background_tasks/balance_monitor.rs b/crates/core/src/background_tasks/balance_monitor.rs index 698f1239..213cde0f 100644 --- a/crates/core/src/background_tasks/balance_monitor.rs +++ b/crates/core/src/background_tasks/balance_monitor.rs @@ -1,4 +1,4 @@ -use alloy::primitives::utils::{format_ether, parse_ether}; +use alloy::primitives::utils::{format_ether, parse_ether, UnitsError}; use alloy::primitives::U256; use std::{sync::Arc, time::Duration}; use tokio::sync::Mutex; @@ -10,11 +10,11 @@ use crate::{ shutdown::subscribe_to_shutdown, webhooks::WebhookManager, }; -fn get_minimum_balance_threshold(chain_id: &ChainId) -> U256 { +fn get_minimum_balance_threshold(chain_id: &ChainId) -> Result { if chain_id.u64() == 1 { - parse_ether("0.005").expect("Failed to parse native token threshold") + parse_ether("0.005") } else { - parse_ether("0.001").expect("Failed to parse native token threshold") + parse_ether("0.001") } } @@ -59,7 +59,8 @@ async fn check_balances_for_chain( webhook_manager: &Option>>, ) -> Result<(), Box> { let chain_id = provider.chain_id; - let min_balance = get_minimum_balance_threshold(&chain_id); + let min_balance = get_minimum_balance_threshold(&chain_id) + .map_err(|e| format!("Failed to parse native token threshold: {}", e))?; let min_balance_formatted = format_ether(min_balance); info!("Checking balances for chain {} (minimum: {} ETH)", chain_id, min_balance_formatted); diff --git a/crates/core/src/gas/fee_estimator/etherscan.rs b/crates/core/src/gas/fee_estimator/etherscan.rs index 4df69e41..26f6a160 100644 --- a/crates/core/src/gas/fee_estimator/etherscan.rs +++ b/crates/core/src/gas/fee_estimator/etherscan.rs @@ -59,7 +59,7 @@ impl EtherscanGasOracleResult { // Calculate priority fees by subtracting base fee from total // Ensure priority fee is at least 1 gwei to avoid zero or negative values - let min_priority_fee = parse_units("1", "gwei").unwrap().try_into().unwrap(); // 1 gwei minimum + let min_priority_fee = Self::parse_gwei_to_wei("1")?; // 1 gwei minimum let safe_priority = std::cmp::max(safe_total.saturating_sub(base_fee), min_priority_fee); let propose_priority = diff --git a/crates/core/src/gas/fee_estimator/fallback.rs b/crates/core/src/gas/fee_estimator/fallback.rs index bd36ecbd..1b9e2904 100644 --- a/crates/core/src/gas/fee_estimator/fallback.rs +++ b/crates/core/src/gas/fee_estimator/fallback.rs @@ -51,6 +51,22 @@ impl FallbackGasFeeEstimator { .into(), }; + let default_priority_fee: u128 = if ethereum_or_ethereum_testnet { + // 2 gwei default for Ethereum + u128::try_from( + parse_units("2", "gwei") + .map_err(|e| GasEstimatorError::CustomError(e.to_string()))?, + ) + .map_err(|e| GasEstimatorError::CustomError(e.to_string()))? + } else { + // 0.01 gwei default for other chains + u128::try_from( + parse_units("0.01", "gwei") + .map_err(|e| GasEstimatorError::CustomError(e.to_string()))?, + ) + .map_err(|e| GasEstimatorError::CustomError(e.to_string()))? + }; + let priority_fee = if let Some(rewards) = &fee_history.reward { if !rewards.is_empty() { let mut all_rewards: Vec = rewards @@ -62,20 +78,14 @@ impl FallbackGasFeeEstimator { all_rewards.sort(); let median_idx = all_rewards.len() / 2; all_rewards[median_idx] - } else if ethereum_or_ethereum_testnet { - parse_units("2", "gwei").unwrap().try_into().unwrap() // 2 gwei default for Ethereum } else { - parse_units("0.01", "gwei").unwrap().try_into().unwrap() + default_priority_fee } - } else if ethereum_or_ethereum_testnet { - parse_units("2", "gwei").unwrap().try_into().unwrap() // 2 gwei default for Ethereum } else { - parse_units("0.01", "gwei").unwrap().try_into().unwrap() // 0.01 gwei default for other chains + default_priority_fee } - } else if ethereum_or_ethereum_testnet { - parse_units("2", "gwei").unwrap().try_into().unwrap() // 2 gwei default for Ethereum } else { - parse_units("0.01", "gwei").unwrap().try_into().unwrap() // 0.01 gwei default for other chains + default_priority_fee }; let max_fee = if chain_id.u64() == 1 { diff --git a/crates/core/src/gas/fee_estimator/tenderly.rs b/crates/core/src/gas/fee_estimator/tenderly.rs index 79ab1577..31d9761d 100644 --- a/crates/core/src/gas/fee_estimator/tenderly.rs +++ b/crates/core/src/gas/fee_estimator/tenderly.rs @@ -426,18 +426,10 @@ impl TenderlyGasFeeEstimator { async fn request_gas_estimate( &self, chain_id: &ChainId, - ) -> Result { - let url = match self.build_suggested_gas_price_endpoint(chain_id) { - Ok(url) => url, - Err(_) => { - let client = reqwest::Client::new(); - let result = client.get("http://").send().await; - match result { - Err(error) => return Err(error), - Ok(_) => unreachable!("This should always fail"), - } - } - }; + ) -> Result { + let url = self + .build_suggested_gas_price_endpoint(chain_id) + .map_err(GasEstimatorError::CustomError)?; println!("Tenderly gas estimate url: {}", url); let client = reqwest::Client::new(); diff --git a/crates/core/src/provider/evm_provider.rs b/crates/core/src/provider/evm_provider.rs index 8d4e70de..c4308645 100644 --- a/crates/core/src/provider/evm_provider.rs +++ b/crates/core/src/provider/evm_provider.rs @@ -652,7 +652,7 @@ mod tests { &self, _chain_id: &ChainId, ) -> Result { - unreachable!("clone_wallet does not estimate gas") + panic!("clone_wallet does not estimate gas") } fn is_chain_supported(&self, _chain_id: &ChainId) -> bool { diff --git a/crates/core/src/rate_limiting/rate_limiter.rs b/crates/core/src/rate_limiting/rate_limiter.rs index 25798cfc..cadccd2f 100644 --- a/crates/core/src/rate_limiting/rate_limiter.rs +++ b/crates/core/src/rate_limiting/rate_limiter.rs @@ -497,7 +497,16 @@ impl RateLimiter { } fn calculate_window_start(&self, current_time: SystemTime, window_seconds: u32) -> SystemTime { - let current_timestamp = current_time.duration_since(UNIX_EPOCH).unwrap().as_secs(); + let current_timestamp = match current_time.duration_since(UNIX_EPOCH) { + Ok(duration) => duration.as_secs(), + Err(e) => { + error!( + "System time is before UNIX_EPOCH ({}); defaulting rate limit window start to epoch", + e + ); + 0 + } + }; let window_start_timestamp = (current_timestamp / window_seconds as u64) * window_seconds as u64; UNIX_EPOCH + std::time::Duration::from_secs(window_start_timestamp) diff --git a/crates/core/src/relayer/cache.rs b/crates/core/src/relayer/cache.rs index 83286ad3..50cc26e2 100644 --- a/crates/core/src/relayer/cache.rs +++ b/crates/core/src/relayer/cache.rs @@ -11,7 +11,8 @@ fn build_relayer_cache_key(id: &RelayerId) -> String { pub async fn get_relayer_cache(cache: &Arc, relayer_id: &RelayerId) -> Option { if let Some(cached_result) = cache.get(&build_relayer_cache_key(relayer_id).to_string()).await { - return cached_result.to_relayer(); + // flatten: a cached `None` relayer and an unexpected cache variant both yield `None` + return cached_result.to_relayer().flatten(); } None diff --git a/crates/core/src/relayer/db/write.rs b/crates/core/src/relayer/db/write.rs index 7cfc3441..591afe7e 100644 --- a/crates/core/src/relayer/db/write.rs +++ b/crates/core/src/relayer/db/write.rs @@ -152,44 +152,69 @@ impl PostgresClient { })?; } CreateRelayerMode::Create => { - let evm_provider_clone = evm_provider.clone(); - let new_relayer_id_val = new_relayer_id; - let name_val = name.to_string(); - let chain_id_val = *chain_id; - - self.with_transaction(move |tx| { - Box::pin(async move { - let query = " - WITH new_wallet_index AS ( - SELECT COALESCE(MAX(wallet_index), -1) + 1 AS wallet_index - FROM relayer.record - WHERE chain_id = $3 - ) - INSERT INTO relayer.record (id, name, chain_id, wallet_index, is_private_key) - SELECT $1, $2, $3, wallet_index, false - FROM new_wallet_index - RETURNING wallet_index"; - - let rows = tx.query(query, &[&new_relayer_id_val, &name_val, &chain_id_val]).await.map_err(PostgresError::PgError)?; - - let wallet_index: i32 = rows.first() - .map(|row| row.get("wallet_index")) - .unwrap_or_else(|| panic!("No wallet index returned")); - - let address = evm_provider_clone.create_wallet(wallet_index as u32).await - .unwrap_or_else(|e| panic!("Wallet creation failed: {}", e)); - - tx.execute( - "UPDATE relayer.record SET address = $1 WHERE chain_id = $2 AND wallet_index = $3", - &[&address, &chain_id_val, &wallet_index], - ) - .await.map_err(PostgresError::PgError)?; + let db_error = |e: PostgresError| { + CreateRelayerError::CouldNotSaveRelayerDb(name.to_string(), *chain_id, e) + }; - Ok(()) - }) - }) + let mut conn = self + .pool + .get() + .await + .map_err(|e| db_error(PostgresError::ConnectionPoolError(e)))?; + let tx = + conn.transaction().await.map_err(|e| db_error(PostgresError::PgError(e)))?; + + let query = " + WITH new_wallet_index AS ( + SELECT COALESCE(MAX(wallet_index), -1) + 1 AS wallet_index + FROM relayer.record + WHERE chain_id = $3 + ) + INSERT INTO relayer.record (id, name, chain_id, wallet_index, is_private_key) + SELECT $1, $2, $3, wallet_index, false + FROM new_wallet_index + RETURNING wallet_index"; + + let rows = tx + .query(query, &[&new_relayer_id, &name, chain_id]) + .await + .map_err(|e| db_error(PostgresError::PgError(e)))?; + + let wallet_index: i32 = + rows.first().map(|row| row.get("wallet_index")).ok_or_else(|| { + error!("No wallet index returned - name: {}, chainId: {}", name, chain_id); + CreateRelayerError::NoSaveRelayerInitInfoReturnedDb( + name.to_string(), + *chain_id, + ) + })?; + + let address = match evm_provider.create_wallet(wallet_index as u32).await { + Ok(address) => address, + Err(e) => { + error!("Wallet creation failed: {}", e); + if let Err(rollback_error) = tx.rollback().await { + error!( + "Failed to rollback relayer creation transaction: {}", + rollback_error + ); + } + return Err(CreateRelayerError::WalletError( + name.to_string(), + *chain_id, + Box::new(e), + )); + } + }; + + tx.execute( + "UPDATE relayer.record SET address = $1 WHERE chain_id = $2 AND wallet_index = $3", + &[&address, chain_id, &wallet_index], + ) .await - .map_err(|e| CreateRelayerError::CouldNotSaveRelayerDb(name.to_string(), *chain_id, e))? + .map_err(|e| db_error(PostgresError::PgError(e)))?; + + tx.commit().await.map_err(|e| db_error(PostgresError::PgError(e)))?; } CreateRelayerMode::PrivateKeyImport(wallet_index) => { // Convert negative wallet index to positive private key index for address lookup diff --git a/crates/core/src/safe_proxy.rs b/crates/core/src/safe_proxy.rs index f1d3959b..63513010 100644 --- a/crates/core/src/safe_proxy.rs +++ b/crates/core/src/safe_proxy.rs @@ -311,7 +311,7 @@ impl SafeProxyManager { // ))), // } - unimplemented!("Safe signatures are not supported yet.") + Err(SafeProxyError::SignatureError("Safe signatures are not supported yet.".to_string())) } /// Creates a Safe transaction with proper nonce and signature, ready for execution. diff --git a/crates/core/src/shared/cache.rs b/crates/core/src/shared/cache.rs index b806ffbe..9071794f 100644 --- a/crates/core/src/shared/cache.rs +++ b/crates/core/src/shared/cache.rs @@ -5,6 +5,7 @@ use std::{ }; use tokio::{sync::Mutex, time::sleep}; +use tracing::warn; use crate::{network::Network, relayer::Relayer, transaction::types::Transaction}; @@ -29,41 +30,56 @@ impl CacheValue { } } - pub fn to_networks(&self) -> Vec { + pub fn to_networks(&self) -> Option> { match self { - CacheValue::Networks(networks) => networks.clone(), - _ => panic!("CacheValue name '{}' not supported on to_networks", self.name()), + CacheValue::Networks(networks) => Some(networks.clone()), + _ => { + warn!("CacheValue name '{}' not supported on to_networks", self.name()); + None + } } } - pub fn to_relayer(&self) -> Option { + pub fn to_relayer(&self) -> Option> { match self { - CacheValue::Relayer(relayer) => relayer.clone(), - _ => panic!("CacheValue name '{}' not supported on to_relayer", self.name()), + CacheValue::Relayer(relayer) => Some(relayer.clone()), + _ => { + warn!("CacheValue name '{}' not supported on to_relayer", self.name()); + None + } } } - pub fn to_is_relayer_api_key(&self) -> bool { + pub fn to_is_relayer_api_key(&self) -> Option { match self { - CacheValue::IsRelayerApiKey(result) => *result, - _ => panic!("CacheValue name '{}' not supported on to_is_relayer_api_key", self.name()), + CacheValue::IsRelayerApiKey(result) => Some(*result), + _ => { + warn!("CacheValue name '{}' not supported on to_is_relayer_api_key", self.name()); + None + } } } - pub fn to_transaction(&self) -> Option { + pub fn to_transaction(&self) -> Option> { match self { - CacheValue::Transaction(transaction) => transaction.clone(), - _ => panic!("CacheValue name '{}' not supported on to_transaction", self.name()), + CacheValue::Transaction(transaction) => Some(transaction.clone()), + _ => { + warn!("CacheValue name '{}' not supported on to_transaction", self.name()); + None + } } } - pub fn to_authentication_challenge(&self) -> String { + pub fn to_authentication_challenge(&self) -> Option { match self { - CacheValue::AuthenticationChallenge(challenge) => challenge.clone(), - _ => panic!( - "CacheValue name '{}' not supported on to_authentication_challenge", - self.name() - ), + CacheValue::AuthenticationChallenge(challenge) => Some(challenge.clone()), + _ => { + warn!( + "CacheValue name '{}' not supported on to_authentication_challenge", + self.name() + ); + None + } } } } diff --git a/crates/core/src/startup.rs b/crates/core/src/startup.rs index f8cdbb68..5bf47037 100644 --- a/crates/core/src/startup.rs +++ b/crates/core/src/startup.rs @@ -273,23 +273,36 @@ async fn start_api( let shutdown_signal = async { let ctrl_c = async { - tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler"); + if let Err(e) = tokio::signal::ctrl_c().await { + error!("failed to install Ctrl+C handler: {}", e); + std::future::pending::<()>().await; + } }; #[cfg(unix)] let terminate = async { - tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) - .expect("failed to install signal handler") - .recv() - .await; + match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { + Ok(mut signal) => { + signal.recv().await; + } + Err(e) => { + error!("failed to install signal handler: {}", e); + std::future::pending::<()>().await; + } + } }; #[cfg(windows)] let terminate = async { - tokio::signal::windows::ctrl_break() - .expect("failed to install Ctrl+Break handler") - .recv() - .await; + match tokio::signal::windows::ctrl_break() { + Ok(mut signal) => { + signal.recv().await; + } + Err(e) => { + error!("failed to install Ctrl+Break handler: {}", e); + std::future::pending::<()>().await; + } + } }; #[cfg(not(any(unix, windows)))] @@ -365,6 +378,9 @@ pub enum StartError { #[error("To run rrelayer you need to define at least one network in the yaml file")] NoNetworksDefinedInYaml, + + #[error("Could not install default Crypto Provider. Are you already using it?")] + CryptoProviderInstallError, } pub async fn start(project_path: &Path) -> Result<(), StartError> { @@ -393,7 +409,7 @@ pub async fn start(project_path: &Path) -> Result<(), StartError> { info!("Applied database schema"); CryptoProvider::install_default(default_provider()) - .expect("Could not install default Crypto Provider. Are you already using it?"); + .map_err(|_| StartError::CryptoProviderInstallError)?; let cache = Arc::new(Cache::new().await); diff --git a/crates/core/src/transaction/cache.rs b/crates/core/src/transaction/cache.rs index 35485ecd..ee9fd6e4 100644 --- a/crates/core/src/transaction/cache.rs +++ b/crates/core/src/transaction/cache.rs @@ -11,7 +11,8 @@ fn build_transaction_cache_key(id: &TransactionId) -> String { pub async fn get_transaction_cache(cache: &Arc, id: &TransactionId) -> Option { if let Some(cached_result) = cache.get(&build_transaction_cache_key(id).to_string()).await { - return cached_result.to_transaction(); + // flatten: a cached `None` transaction and an unexpected cache variant both yield `None` + return cached_result.to_transaction().flatten(); } None diff --git a/crates/core/src/transaction/queue_system/transactions_queues.rs b/crates/core/src/transaction/queue_system/transactions_queues.rs index ba7d07b1..15327c18 100644 --- a/crates/core/src/transaction/queue_system/transactions_queues.rs +++ b/crates/core/src/transaction/queue_system/transactions_queues.rs @@ -248,25 +248,35 @@ impl TransactionsQueues { &self, current_transaction: &mut Transaction, replace_with: &RelayTransactionRequest, - ) { - current_transaction.to = replace_with.to; - current_transaction.data = replace_with.data.clone(); - current_transaction.value = replace_with.value; - current_transaction.is_noop = current_transaction.from == current_transaction.to; - - if let Some(ref blob_strings) = replace_with.blobs { - current_transaction.blobs = Some( + ) -> Result<(), ReplaceTransactionError> { + let blobs = replace_with + .blobs + .as_ref() + .map(|blob_strings| { blob_strings .iter() .map(|blob_hex| TransactionBlob::from_hex(blob_hex)) .collect::, _>>() - .expect("Failed to convert blob hex strings to TransactionBlob"), - ); - } else { - current_transaction.blobs = None; - } + }) + .transpose() + .map_err(|e| { + ReplaceTransactionError::SendTransactionError( + TransactionQueueSendTransactionError::TransactionConversionError(format!( + "Failed to convert blob hex strings to TransactionBlob: {}", + e + )), + ) + })?; + + current_transaction.to = replace_with.to; + current_transaction.data = replace_with.data.clone(); + current_transaction.value = replace_with.value; + current_transaction.is_noop = current_transaction.from == current_transaction.to; + current_transaction.blobs = blobs; current_transaction.gas_limit = None; current_transaction.external_id = replace_with.external_id.clone(); + + Ok(()) } /// Computes gas prices for a transaction based on its type. @@ -771,7 +781,7 @@ impl TransactionsQueues { match result.type_name { EditableTransactionType::Pending => { let original_transaction = result.transaction.clone(); - self.transaction_replace(&mut result.transaction, replace_with); + self.transaction_replace(&mut result.transaction, replace_with)?; self.invalidate_transaction_cache(&transaction.id).await; if let Some(webhook_manager) = &self.webhook_manager { diff --git a/crates/core/src/wallet/aws_kms_wallet_manager.rs b/crates/core/src/wallet/aws_kms_wallet_manager.rs index 8de97d4c..98a4b444 100644 --- a/crates/core/src/wallet/aws_kms_wallet_manager.rs +++ b/crates/core/src/wallet/aws_kms_wallet_manager.rs @@ -314,7 +314,11 @@ impl AwsKmsWalletManager { .policy(policy.clone()); for (k, v) in &plan.tags { - let tag = Tag::builder().tag_key(k).tag_value(v).build().unwrap(); + let tag = Tag::builder().tag_key(k).tag_value(v).build().map_err(|e| { + WalletError::ApiError { + message: format!("Failed to build KMS tag ({}={}): {}", k, v, e), + } + })?; create_key_builder = create_key_builder.tags(tag); } diff --git a/crates/core/src/wallet/turnkey_wallet_manager.rs b/crates/core/src/wallet/turnkey_wallet_manager.rs index a01c324c..d5dae269 100644 --- a/crates/core/src/wallet/turnkey_wallet_manager.rs +++ b/crates/core/src/wallet/turnkey_wallet_manager.rs @@ -160,8 +160,14 @@ impl TurnkeyWalletManager { Ok(encoded_stamp) } - fn get_timestamp_ms() -> String { - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis().to_string() + fn get_timestamp_ms() -> Result { + Ok(SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| WalletError::ApiError { + message: format!("System time is before UNIX epoch: {}", e), + })? + .as_millis() + .to_string()) } async fn load_accounts(&self) -> Result<(), WalletError> { @@ -246,7 +252,7 @@ impl WalletManagerTrait for TurnkeyWalletManager { } } - let timestamp = Self::get_timestamp_ms(); + let timestamp = Self::get_timestamp_ms()?; let request = serde_json::json!({ "type": "ACTIVITY_TYPE_CREATE_WALLET_ACCOUNTS", "timestampMs": timestamp, @@ -461,7 +467,7 @@ impl WalletManagerTrait for TurnkeyWalletManager { info!("Turnkey sign_transaction: unsigned transaction hex: {}", unsigned_transaction_hex); - let timestamp = Self::get_timestamp_ms(); + let timestamp = Self::get_timestamp_ms()?; let request = serde_json::json!({ "type": "ACTIVITY_TYPE_SIGN_TRANSACTION_V2", "organizationId": self.organization_id, @@ -608,7 +614,7 @@ impl WalletManagerTrait for TurnkeyWalletManager { info!("Turnkey sign_text: message='{}', hash=0x{}", message, hex::encode(message_hash)); - let timestamp = Self::get_timestamp_ms(); + let timestamp = Self::get_timestamp_ms()?; let request = serde_json::json!({ "type": "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2", "organizationId": self.organization_id, @@ -732,7 +738,7 @@ impl WalletManagerTrait for TurnkeyWalletManager { WalletError::ApiError { message: format!("Failed to serialize typed data: {}", e) } })?; - let timestamp = Self::get_timestamp_ms(); + let timestamp = Self::get_timestamp_ms()?; let accounts = self.accounts.lock().await; let account = accounts.get(&wallet_index).ok_or_else(|| { error!("Turnkey sign_typed_data: wallet not found for index {}", wallet_index); diff --git a/crates/core/src/yaml.rs b/crates/core/src/yaml.rs index 7eb25fb8..2b489006 100644 --- a/crates/core/src/yaml.rs +++ b/crates/core/src/yaml.rs @@ -669,7 +669,13 @@ impl<'de> Deserialize<'de> for AllOrOneOrManyAddresses { match addresses.len() { 0 => Ok(AllOrOneOrManyAddresses::Many(addresses)), // Empty array - 1 => Ok(AllOrOneOrManyAddresses::One(addresses.into_iter().next().unwrap())), + 1 => { + let address = addresses + .into_iter() + .next() + .ok_or_else(|| de::Error::custom("expected a single address"))?; + Ok(AllOrOneOrManyAddresses::One(address)) + } _ => Ok(AllOrOneOrManyAddresses::Many(addresses)), } } @@ -1082,18 +1088,25 @@ pub struct SetupConfig { pub cron_jobs: Option>, } -fn substitute_env_variables(contents: &str) -> Result { +fn substitute_env_variables(contents: &str) -> Result { let re = Regex::new(r"\$\{([^}]+)\}")?; + let mut missing_var: Option = None; let result = re.replace_all(contents, |caps: &Captures| { let var_name = &caps[1]; match env::var(var_name) { Ok(val) => val, Err(_) => { rrelayer_error!("Environment variable {} not found", var_name); - panic!("Environment variable {} not found", var_name) + if missing_var.is_none() { + missing_var = Some(var_name.to_string()); + } + String::new() } } }); + if let Some(var_name) = missing_var { + return Err(ReadYamlError::EnvironmentVariableNotFound(var_name)); + } Ok(result.into_owned()) } @@ -1108,8 +1121,11 @@ pub enum ReadYamlError { #[error("Setup config is invalid yaml and does not match the struct - {0}")] SetupConfigInvalidYaml(String), - #[error("Environment variable {} not found", {0})] - EnvironmentVariableNotFound(#[from] regex::Error), + #[error("Environment variable {0} not found")] + EnvironmentVariableNotFound(String), + + #[error("Invalid environment variable substitution pattern: {0}")] + EnvironmentVariableRegex(#[from] regex::Error), #[error("No networks enabled in the yaml")] NoNetworksEnabled, diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 7d44b2f3..12d6587c 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -30,3 +30,6 @@ rrelayer_core = { path = "../core" } tokio = { version = "1", features = ["time"] } async-trait = "0.1" hex = "0.4" + +[lints] +workspace = true diff --git a/crates/sdk/src/api/http.rs b/crates/sdk/src/api/http.rs index 1650c284..7c271b03 100644 --- a/crates/sdk/src/api/http.rs +++ b/crates/sdk/src/api/http.rs @@ -26,7 +26,7 @@ impl HttpClient { ) } - fn build_headers(&self, additional_headers: Option) -> HeaderMap { + fn build_headers(&self, additional_headers: Option) -> ApiResult { let mut headers = HeaderMap::new(); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); @@ -36,12 +36,19 @@ impl HttpClient { let encoded = general_purpose::STANDARD.encode(credentials); headers.insert( AUTHORIZATION, - HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(), + HeaderValue::from_str(&format!("Basic {}", encoded)).map_err(|e| { + ApiSdkError::ConfigError(format!("Invalid basic auth header value: {}", e)) + })?, ); } AuthConfig::ApiKey { api_key } => { let header_name = HeaderName::from_static("x-rrelayer-api-key"); - headers.insert(header_name, HeaderValue::from_str(api_key).unwrap()); + headers.insert( + header_name, + HeaderValue::from_str(api_key).map_err(|e| { + ApiSdkError::ConfigError(format!("Invalid API key header value: {}", e)) + })?, + ); } } @@ -53,7 +60,7 @@ impl HttpClient { } } - headers + Ok(headers) } fn handle_response_status(&self, response: &reqwest::Response) -> ApiResult<()> { @@ -70,7 +77,7 @@ impl HttpClient { T: DeserializeOwned, { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.get(&url).headers(headers).send().await?; self.handle_response_status(&response)?; @@ -84,7 +91,7 @@ impl HttpClient { T: DeserializeOwned, { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.get(&url).headers(headers).send().await?; @@ -104,7 +111,7 @@ impl HttpClient { Q: Serialize, { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let mut request = self.client.get(&url).headers(headers); if let Some(q) = query { @@ -123,7 +130,7 @@ impl HttpClient { B: Serialize, { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.post(&url).headers(headers).json(body).send().await?; self.handle_response_status(&response)?; @@ -143,7 +150,7 @@ impl HttpClient { B: Serialize, { let url = self.build_url(endpoint); - let headers = self.build_headers(Some(headers)); + let headers = self.build_headers(Some(headers))?; let response = self.client.post(&url).headers(headers).json(body).send().await?; self.handle_response_status(&response)?; @@ -157,7 +164,7 @@ impl HttpClient { B: Serialize, { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.post(&url).headers(headers).json(body).send().await?; self.handle_response_status(&response)?; @@ -172,7 +179,7 @@ impl HttpClient { B: Serialize, { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.put(&url).headers(headers).json(body).send().await?; self.handle_response_status(&response)?; @@ -192,7 +199,7 @@ impl HttpClient { B: Serialize, { let url = self.build_url(endpoint); - let headers = self.build_headers(Some(headers)); + let headers = self.build_headers(Some(headers))?; let response = self.client.put(&url).headers(headers).json(body).send().await?; self.handle_response_status(&response)?; @@ -206,7 +213,7 @@ impl HttpClient { B: Serialize, { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.put(&url).headers(headers).json(body).send().await?; self.handle_response_status(&response)?; @@ -220,7 +227,7 @@ impl HttpClient { T: DeserializeOwned, { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.delete(&url).headers(headers).send().await?; self.handle_response_status(&response)?; @@ -231,7 +238,7 @@ impl HttpClient { pub async fn delete_status(&self, endpoint: &str) -> ApiResult<()> { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.delete(&url).headers(headers).send().await?; self.handle_response_status(&response)?; @@ -246,7 +253,7 @@ impl HttpClient { B: Serialize, { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.delete(&url).headers(headers).json(body).send().await?; self.handle_response_status(&response)?; @@ -257,7 +264,7 @@ impl HttpClient { pub async fn get_status(&self, endpoint: &str) -> ApiResult<()> { let url = self.build_url(endpoint); - let headers = self.build_headers(None); + let headers = self.build_headers(None)?; let response = self.client.get(&url).headers(headers).send().await?; self.handle_response_status(&response)?; diff --git a/crates/sdk/src/api/sign/mod.rs b/crates/sdk/src/api/sign/mod.rs index cdc51d83..64d1b93a 100644 --- a/crates/sdk/src/api/sign/mod.rs +++ b/crates/sdk/src/api/sign/mod.rs @@ -1,4 +1,7 @@ -use crate::api::{http::HttpClient, types::ApiResult}; +use crate::api::{ + http::HttpClient, + types::{ApiResult, ApiSdkError}, +}; use reqwest::header::{HeaderMap, HeaderValue}; use rrelayer_core::RATE_LIMIT_HEADER_NAME; use rrelayer_core::common_types::{PagingContext, PagingResult}; @@ -27,7 +30,9 @@ impl SignApi { if let Some(rate_limit_key) = rate_limit_key.as_ref() { headers.insert( RATE_LIMIT_HEADER_NAME, - HeaderValue::from_str(rate_limit_key).expect("Invalid rate limit key"), + HeaderValue::from_str(rate_limit_key).map_err(|e| { + ApiSdkError::ConfigError(format!("Invalid rate limit key: {}", e)) + })?, ); } @@ -50,7 +55,9 @@ impl SignApi { if let Some(rate_limit_key) = rate_limit_key.as_ref() { headers.insert( RATE_LIMIT_HEADER_NAME, - HeaderValue::from_str(rate_limit_key).expect("Invalid rate limit key"), + HeaderValue::from_str(rate_limit_key).map_err(|e| { + ApiSdkError::ConfigError(format!("Invalid rate limit key: {}", e)) + })?, ); } diff --git a/crates/sdk/src/api/transaction/mod.rs b/crates/sdk/src/api/transaction/mod.rs index 0f760dac..db160954 100644 --- a/crates/sdk/src/api/transaction/mod.rs +++ b/crates/sdk/src/api/transaction/mod.rs @@ -58,7 +58,9 @@ impl TransactionApi { if let Some(rate_limit_key) = rate_limit_key.as_ref() { headers.insert( RATE_LIMIT_HEADER_NAME, - HeaderValue::from_str(rate_limit_key).expect("Invalid rate limit key"), + HeaderValue::from_str(rate_limit_key).map_err(|e| { + ApiSdkError::ConfigError(format!("Invalid rate limit key: {}", e)) + })?, ); } self.client @@ -80,7 +82,9 @@ impl TransactionApi { if let Some(rate_limit_key) = rate_limit_key.as_ref() { headers.insert( RATE_LIMIT_HEADER_NAME, - HeaderValue::from_str(rate_limit_key).expect("Invalid rate limit key"), + HeaderValue::from_str(rate_limit_key).map_err(|e| { + ApiSdkError::ConfigError(format!("Invalid rate limit key: {}", e)) + })?, ); } self.client @@ -101,7 +105,9 @@ impl TransactionApi { if let Some(rate_limit_key) = rate_limit_key.as_ref() { headers.insert( RATE_LIMIT_HEADER_NAME, - HeaderValue::from_str(rate_limit_key).expect("Invalid rate limit key"), + HeaderValue::from_str(rate_limit_key).map_err(|e| { + ApiSdkError::ConfigError(format!("Invalid rate limit key: {}", e)) + })?, ); } @@ -120,7 +126,9 @@ impl TransactionApi { if let Some(rate_limit_key) = rate_limit_key.as_ref() { headers.insert( RATE_LIMIT_HEADER_NAME, - HeaderValue::from_str(rate_limit_key).expect("Invalid rate limit key"), + HeaderValue::from_str(rate_limit_key).map_err(|e| { + ApiSdkError::ConfigError(format!("Invalid rate limit key: {}", e)) + })?, ); } From 385d9340fc92aadf1abba386dc8dba79ea8cc5fd Mon Sep 17 00:00:00 2001 From: Josh Stevens Date: Sun, 9 Aug 2026 12:59:54 -0400 Subject: [PATCH 2/2] docs: add changelog entry for clippy panic ban Co-Authored-By: Claude Fable 5 --- documentation/rrelayer/docs/pages/changelog.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/rrelayer/docs/pages/changelog.mdx b/documentation/rrelayer/docs/pages/changelog.mdx index 4b676785..73afd851 100644 --- a/documentation/rrelayer/docs/pages/changelog.mdx +++ b/documentation/rrelayer/docs/pages/changelog.mdx @@ -10,6 +10,7 @@ ### Bug fixes +- fix: remove all panic/unwrap/expect paths from production code, replacing them with proper error propagation, and enforce the ban going forward with workspace-level clippy deny lints - fix: deploy documentation through github pages - fix: prevent docker cache export failures from blocking arm64 image publishing