diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index c95d2a0a40..47722ccab2 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -84,6 +84,7 @@ use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, BigUint, MmNumber}; use rand::seq::SliceRandom; +use regex::Regex; use rlp::{DecoderError, Encodable, RlpStream}; use rpc::v1::types::Bytes as BytesJson; use secp256k1::PublicKey; @@ -6843,6 +6844,17 @@ pub enum EthGasDetailsErr { threshold )] AmountTooLow { amount: BigDecimal, threshold: BigDecimal }, + #[display( + fmt = "Provided gas fee cap {} Gwei is too low, the required network base fee is {} Gwei.", + provided_fee_cap, + required_base_fee + )] + GasFeeCapTooLow { + provided_fee_cap: BigDecimal, + required_base_fee: BigDecimal, + }, + #[display(fmt = "The provided 'max_fee_per_gas' is below the current block's base fee.")] + GasFeeCapBelowBaseFee, #[from_stringify("NumConversError")] #[display(fmt = "Internal error: {}", _0)] Internal(String), @@ -6869,6 +6881,19 @@ impl From for EthGasDetailsErr { } } +fn parse_fee_cap_error(message: &str) -> Option<(U256, U256)> { + let re = Regex::new(r"gasfeecap: (\d+)\s+basefee: (\d+)").ok()?; + let caps = re.captures(message)?; + + let user_cap_str = caps.get(1)?.as_str(); + let required_base_str = caps.get(2)?.as_str(); + + let user_cap = U256::from_dec_str(user_cap_str).ok()?; + let required_base = U256::from_dec_str(required_base_str).ok()?; + + Some((user_cap, required_base)) +} + async fn get_eth_gas_details_from_withdraw_fee( eth_coin: &EthCoin, fee: Option, @@ -6960,6 +6985,20 @@ async fn get_eth_gas_details_from_withdraw_fee( let amount = u256_to_big_decimal(eth_value, eth_coin.decimals).map_mm_err()?; return MmError::err(EthGasDetailsErr::AmountTooLow { amount, threshold }); + } else if error_str.contains("fee cap less than block base fee") + || error_str.contains("max fee per gas less than block base fee") + { + if let Some((user_cap, required_base)) = parse_fee_cap_error(&error_str) { + // The RPC error gives fee values in wei. Convert to Gwei (9 decimals) for the user. + let provided_fee_cap = u256_to_big_decimal(user_cap, ETH_GWEI_DECIMALS).map_mm_err()?; + let required_base_fee = u256_to_big_decimal(required_base, ETH_GWEI_DECIMALS).map_mm_err()?; + return MmError::err(EthGasDetailsErr::GasFeeCapTooLow { + provided_fee_cap, + required_base_fee, + }); + } else { + return MmError::err(EthGasDetailsErr::GasFeeCapBelowBaseFee); + } } // This can be a transport error or a non-standard insufficient funds error. // In the latter case, diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 09a638c0fd..1626e9d8af 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -3108,6 +3108,12 @@ pub enum WithdrawError { InvalidAddress(String), #[display(fmt = "Invalid fee policy: {}", _0)] InvalidFeePolicy(String), + #[display(fmt = "Invalid fee parameters: {}", reason)] + InvalidFee { + reason: String, + #[serde(skip_serializing_if = "Option::is_none")] + details: Option, + }, #[display(fmt = "Invalid memo field: {}", _0)] InvalidMemo(String), #[display(fmt = "No such coin {}", coin)] @@ -3201,6 +3207,7 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::AmountTooLow { .. } | WithdrawError::InvalidAddress(_) | WithdrawError::InvalidFeePolicy(_) + | WithdrawError::InvalidFee { .. } | WithdrawError::InvalidMemo(_) | WithdrawError::FromAddressNotFound | WithdrawError::UnexpectedFromAddress(_) @@ -3296,6 +3303,24 @@ impl From for WithdrawError { match e { EthGasDetailsErr::InvalidFeePolicy(e) => WithdrawError::InvalidFeePolicy(e), EthGasDetailsErr::AmountTooLow { amount, threshold } => WithdrawError::AmountTooLow { amount, threshold }, + EthGasDetailsErr::GasFeeCapTooLow { + provided_fee_cap, + required_base_fee, + } => { + let reason = "Provided gas fee cap is less than the required network base fee.".to_string(); + let details = json!({ + "provided_fee_cap_gwei": provided_fee_cap.to_string(), + "required_base_fee_gwei": required_base_fee.to_string() + }); + WithdrawError::InvalidFee { + reason, + details: Some(details), + } + }, + EthGasDetailsErr::GasFeeCapBelowBaseFee => { + let reason = "The provided 'max fee per gas' is too low for current network conditions.".to_string(); + WithdrawError::InvalidFee { reason, details: None } + }, EthGasDetailsErr::Internal(e) => WithdrawError::InternalError(e), EthGasDetailsErr::Transport(e) => WithdrawError::Transport(e), EthGasDetailsErr::NftProtocolNotSupported => WithdrawError::NftProtocolNotSupported,