diff --git a/Cargo.lock b/Cargo.lock index 750b54b0e..592f9e00a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6826,6 +6826,7 @@ dependencies = [ "sui-transaction-builder", "tokio", "tracing", + "typeshare", ] [[package]] diff --git a/crates/swapper/Cargo.toml b/crates/swapper/Cargo.toml index 59a8d9e50..e06d79017 100644 --- a/crates/swapper/Cargo.toml +++ b/crates/swapper/Cargo.toml @@ -32,6 +32,7 @@ serde_serializers = { path = "../serde_serializers" } number_formatter = { path = "../number_formatter" } reqwest = { workspace = true, optional = true } +typeshare = { version = "1.0.4" } bcs.workspace = true sui-types = { workspace = true } diff --git a/crates/swapper/src/across/api.rs b/crates/swapper/src/across/api.rs index 359ceeae8..927919b05 100644 --- a/crates/swapper/src/across/api.rs +++ b/crates/swapper/src/across/api.rs @@ -61,10 +61,10 @@ impl AcrossApi { .map(|topic| topic.eq_ignore_ascii_case(FUNDS_DEPOSITED_TOPIC)) .unwrap_or(false) }) - .ok_or_else(|| SwapperError::NetworkError("FundsDeposited event not found".into()))?; + .ok_or_else(|| SwapperError::TransactionError("FundsDeposited event not found".into()))?; if deposit_log.topics.len() < 3 { - return Err(SwapperError::NetworkError("invalid FundsDeposited topics".into())); + return Err(SwapperError::TransactionError("invalid FundsDeposited topics".into())); } // The deposit ID is in topics[2] (topics[0] is event signature, topics[1] is destination chain ID) let deposit_id_hex = deposit_log.topics[2].clone(); @@ -72,7 +72,7 @@ impl AcrossApi { // Convert hex deposit ID to decimal string let deposit_id = if let Some(stripped) = deposit_id_hex.strip_prefix("0x") { u64::from_str_radix(stripped, 16) - .map_err(|e| SwapperError::NetworkError(format!("Failed to parse deposit ID: {}", e)))? + .map_err(|e| SwapperError::TransactionError(format!("Failed to parse deposit ID: {}", e)))? .to_string() } else { deposit_id_hex diff --git a/crates/swapper/src/across/config_store.rs b/crates/swapper/src/across/config_store.rs index 8cf64d82d..9afbb5ab2 100644 --- a/crates/swapper/src/across/config_store.rs +++ b/crates/swapper/src/across/config_store.rs @@ -74,7 +74,7 @@ impl ConfigStoreClient { let result: TokenConfig = serde_json::from_str(&decoded).map_err(SwapperError::from)?; Ok(result) } else { - Err(SwapperError::ABIError("config call failed".into())) + Err(SwapperError::ComputeQuoteError("config call failed".into())) } } @@ -83,7 +83,7 @@ impl ConfigStoreClient { let call = EthereumRpc::Call(TransactionObject::new_call(&self.contract, data), BlockParameter::Latest); let response: JsonRpcResult = self.client.call_with_cache(&call, Some(CONFIG_CACHE_TTL)).await?; let result = response.take()?; - let hex_data = HexDecode(result).map_err(|e| SwapperError::NetworkError(e.to_string()))?; + let hex_data = HexDecode(result).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?; let decoded = AcrossConfigStore::l1TokenConfigCall::abi_decode_returns(&hex_data).map_err(SwapperError::from)?; let result: TokenConfig = serde_json::from_str(&decoded).map_err(SwapperError::from)?; diff --git a/crates/swapper/src/across/hubpool.rs b/crates/swapper/src/across/hubpool.rs index b0f77b3fd..9a3e96e3c 100644 --- a/crates/swapper/src/across/hubpool.rs +++ b/crates/swapper/src/across/hubpool.rs @@ -37,7 +37,7 @@ impl HubPoolClient { } pub fn decoded_paused_call3(&self, result: &IMulticall3::Result) -> Result { - let value = decode_call3_return::(result).map_err(|e| SwapperError::ABIError(e.to_string()))?; + let value = decode_call3_return::(result).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?; Ok(value) } @@ -59,10 +59,11 @@ impl HubPoolClient { pub fn decoded_pooled_token_call3(&self, result: &IMulticall3::Result) -> Result { if result.success { - let decoded = HubPoolInterface::pooledTokensCall::abi_decode_returns(&result.returnData).map_err(|e| SwapperError::ABIError(e.to_string()))?; + let decoded = + HubPoolInterface::pooledTokensCall::abi_decode_returns(&result.returnData).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?; Ok(decoded) } else { - Err(SwapperError::ABIError("pooled token call failed".into())) + Err(SwapperError::ComputeQuoteError("pooled token call failed".into())) } } @@ -89,7 +90,7 @@ impl HubPoolClient { Ok(BigInt::from_bytes_le(Sign::Plus, &value.to_le_bytes::<32>())) } else { - Err(SwapperError::ABIError("utilization call failed".into())) + Err(SwapperError::ComputeQuoteError("utilization call failed".into())) } } @@ -98,15 +99,17 @@ impl HubPoolClient { } pub fn decoded_current_time(&self, result: &IMulticall3::Result) -> Result { - let value = decode_call3_return::(result).map_err(|e| SwapperError::ABIError(e.to_string()))?; - value.try_into().map_err(|_| SwapperError::ABIError("decode current time failed".into())) + let value = decode_call3_return::(result).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?; + value + .try_into() + .map_err(|_| SwapperError::ComputeQuoteError("decode current time failed".into())) } pub async fn fetch_utilization(&self, pool_token: &Address, amount: U256) -> Result { let call3 = self.utilization_call3(pool_token, amount); let call = EthereumRpc::Call(TransactionObject::new_call(&self.contract, call3.callData.to_vec()), BlockParameter::Latest); let result: String = self.client.request(call).await?; - let hex_data = HexDecode(result).map_err(|e| SwapperError::NetworkError(e.to_string()))?; + let hex_data = HexDecode(result).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?; let value = HubPoolInterface::liquidityUtilizationCurrentCall::abi_decode_returns(&hex_data).map_err(SwapperError::from)?; let result = BigInt::from_bytes_le(num_bigint::Sign::Plus, &value.to_le_bytes::<32>()); Ok(result) diff --git a/crates/swapper/src/across/provider.rs b/crates/swapper/src/across/provider.rs index 91fed5b29..945151d19 100644 --- a/crates/swapper/src/across/provider.rs +++ b/crates/swapper/src/across/provider.rs @@ -96,17 +96,17 @@ impl Across { create_eth_client(self.rpc_provider.clone(), chain)? .multicall3(calls) .await - .map_err(|e| SwapperError::NetworkError(e.to_string())) + .map_err(|e| SwapperError::ComputeQuoteError(e.to_string())) } async fn estimate_gas_transaction(&self, chain: Chain, tx: TransactionObject) -> Result { - let client = create_eth_client(self.rpc_provider.clone(), chain)?; + let client = create_eth_client(self.rpc_provider.clone(), chain).map_err(SwapperError::into_transaction_error)?; let gas_hex = client .estimate_gas(tx.from.as_deref(), &tx.to, tx.value.as_deref(), Some(tx.data.as_str())) .await - .map_err(SwapperError::from)?; + .map_err(|err| SwapperError::TransactionError(err.to_string()))?; - let gas_biguint = biguint_from_hex_str(&gas_hex).map_err(|e| SwapperError::NetworkError(format!("Failed to parse gas estimate: {e}")))?; + let gas_biguint = biguint_from_hex_str(&gas_hex).map_err(|e| SwapperError::TransactionError(format!("Failed to parse gas estimate: {e}")))?; let gas_bigint = BigInt::from_biguint(Sign::Plus, gas_biguint); Self::bigint_to_u256(&gas_bigint) } @@ -319,7 +319,7 @@ impl Swapper for Across { async fn fetch_quote(&self, request: &QuoteRequest) -> Result { // does not support same chain swap if request.from_asset.chain() == request.to_asset.chain() { - return Err(SwapperError::NotSupportedPair); + return Err(SwapperError::NotSupportedAsset); } let input_is_native = request.from_asset.is_native(); @@ -330,11 +330,11 @@ impl Swapper for Across { let _ = AcrossDeployment::deployment_by_chain(&request.from_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; let destination_deployment = AcrossDeployment::deployment_by_chain(&request.to_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; if !Self::is_supported_pair(&request.from_asset.asset_id(), &request.to_asset.asset_id()) { - return Err(SwapperError::NotSupportedPair); + return Err(SwapperError::NotSupportedAsset); } - let input_asset = eth_address::convert_native_to_weth(&request.from_asset.asset_id()).ok_or(SwapperError::NotSupportedPair)?; - let output_asset = eth_address::convert_native_to_weth(&request.to_asset.asset_id()).ok_or(SwapperError::NotSupportedPair)?; + let input_asset = eth_address::convert_native_to_weth(&request.from_asset.asset_id()).ok_or(SwapperError::NotSupportedAsset)?; + let output_asset = eth_address::convert_native_to_weth(&request.to_asset.asset_id()).ok_or(SwapperError::NotSupportedAsset)?; let original_output_asset = request.to_asset.asset_id(); let output_token = eth_address::parse_asset_id(&output_asset)?; @@ -426,7 +426,7 @@ impl Swapper for Across { // Check if bridge amount is too small if remain_amount < gas_fee { - return Err(SwapperError::InputAmountTooSmall); + return Err(SwapperError::InputAmountError { min_amount: None }); } let output_amount = remain_amount - gas_fee; diff --git a/crates/swapper/src/approval/evm.rs b/crates/swapper/src/approval/evm.rs index f5839608a..3ff4843cc 100644 --- a/crates/swapper/src/approval/evm.rs +++ b/crates/swapper/src/approval/evm.rs @@ -50,15 +50,27 @@ pub async fn check_approval_erc20_with_client( where C: Client + Clone + std::fmt::Debug + Send + Sync + 'static, { - let owner: Address = owner.as_str().parse().map_err(|_| SwapperError::InvalidAddress(owner))?; - let spender: Address = spender.as_str().parse().map_err(|_| SwapperError::InvalidAddress(spender))?; + let owner: Address = owner + .as_str() + .parse() + .map_err(|_| SwapperError::TransactionError(format!("Invalid address {owner}")))?; + let spender: Address = spender + .as_str() + .parse() + .map_err(|_| SwapperError::TransactionError(format!("Invalid address {spender}")))?; let allowance_data = IERC20::allowanceCall { owner, spender }.abi_encode(); let allowance_call = EthereumRpc::Call(TransactionObject::new_call(&token, allowance_data), BlockParameter::Latest); - let result: String = client.request(allowance_call).await.map_err(SwapperError::from)?; - let decoded = HexDecode(result).map_err(|_| SwapperError::ABIError("failed to decode allowance_call result".into()))?; + let result: String = client + .request(allowance_call) + .await + .map_err(SwapperError::from) + .map_err(SwapperError::into_transaction_error)?; + let decoded = HexDecode(result).map_err(|_| SwapperError::TransactionError("failed to decode allowance_call result".into()))?; - let allowance = IERC20::allowanceCall::abi_decode_returns(&decoded).map_err(SwapperError::from)?; + let allowance = IERC20::allowanceCall::abi_decode_returns(&decoded) + .map_err(SwapperError::from) + .map_err(SwapperError::into_transaction_error)?; if allowance < amount { return Ok(ApprovalType::Approve(ApprovalData { token: token.to_string(), @@ -101,15 +113,21 @@ where .abi_encode(); let permit2_call = EthereumRpc::Call(TransactionObject::new_call(permit2_contract, permit2_data), BlockParameter::Latest); - let result: String = client.request(permit2_call).await.map_err(SwapperError::from)?; - let decoded = HexDecode(result).map_err(|_| SwapperError::ABIError("failed to decode permit2 allowance result".into()))?; - let allowance_return = IAllowanceTransfer::allowanceCall::abi_decode_returns(&decoded).map_err(SwapperError::from)?; + let result: String = client + .request(permit2_call) + .await + .map_err(SwapperError::from) + .map_err(SwapperError::into_transaction_error)?; + let decoded = HexDecode(result).map_err(|_| SwapperError::TransactionError("failed to decode permit2 allowance result".into()))?; + let allowance_return = IAllowanceTransfer::allowanceCall::abi_decode_returns(&decoded) + .map_err(SwapperError::from) + .map_err(SwapperError::into_transaction_error)?; let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs(); let expiration: u64 = allowance_return ._1 .try_into() - .map_err(|_| SwapperError::ABIError("failed to convert expiration to u64".into()))?; + .map_err(|_| SwapperError::TransactionError("failed to convert expiration to u64".into()))?; if U256::from(allowance_return._0) < amount || expiration < timestamp { return Ok(ApprovalType::Permit2(Permit2ApprovalData { @@ -120,7 +138,7 @@ where permit2_nonce: allowance_return ._2 .try_into() - .map_err(|_| SwapperError::ABIError("failed to convert nonce to u64".into()))?, + .map_err(|_| SwapperError::TransactionError("failed to convert nonce to u64".into()))?, })); } diff --git a/crates/swapper/src/approval/tron.rs b/crates/swapper/src/approval/tron.rs index 46f858349..d1783e296 100644 --- a/crates/swapper/src/approval/tron.rs +++ b/crates/swapper/src/approval/tron.rs @@ -12,11 +12,11 @@ pub async fn check_approval_tron( amount: U256, provider: Arc, ) -> Result { - let client = create_tron_client(provider.clone()).map_err(|e| SwapperError::NetworkError(e.to_string()))?; + let client = create_tron_client(provider.clone()).map_err(|e| SwapperError::TransactionError(e.to_string()))?; let allowance = client .get_token_allowance(owner_address, token_address, spender_address) .await - .map_err(|e| SwapperError::NetworkError(e.to_string()))?; + .map_err(|e| SwapperError::TransactionError(e.to_string()))?; let amount_big = BigUint::from_bytes_be(&amount.to_be_bytes::<32>()); if allowance < amount_big { return Ok(ApprovalType::Approve(ApprovalData { diff --git a/crates/swapper/src/cetus/provider.rs b/crates/swapper/src/cetus/provider.rs index f9908241d..b156d7557 100644 --- a/crates/swapper/src/cetus/provider.rs +++ b/crates/swapper/src/cetus/provider.rs @@ -186,7 +186,10 @@ where Some(ObjectDataOptions::default()), ); - let pool_datas: Vec = sui_client.rpc_call(rpc_call).await.map_err(|e| SwapperError::NetworkError(e.to_string()))?; + let pool_datas: Vec = sui_client + .rpc_call(rpc_call) + .await + .map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?; let pool_quotes = top_pools .into_iter() @@ -287,11 +290,11 @@ where // Execute gas_price and coin_assets fetching in parallel let (gas_price_result, all_coin_assets_result) = join!(sui_client.get_gas_price(), sui_client.get_coin_assets(sender_address)); - let gas_price_bigint = gas_price_result.map_err(|e| SwapperError::NetworkError(e.to_string()))?; + let gas_price_bigint = gas_price_result.map_err(|e| SwapperError::TransactionError(e.to_string()))?; let gas_price = gas_price_bigint .to_u64() - .ok_or_else(|| SwapperError::NetworkError("Failed to convert gas price to u64".into()))?; - let all_coin_assets = all_coin_assets_result.map_err(|e| SwapperError::NetworkError(e.to_string()))?; + .ok_or_else(|| SwapperError::TransactionError("Failed to convert gas price to u64".into()))?; + let all_coin_assets = all_coin_assets_result.map_err(|e| SwapperError::TransactionError(e.to_string()))?; // Prepare swap params for tx building let a2b = from_coin == route_data.coin_a; @@ -323,7 +326,7 @@ where let inspect_result = sui_client .inspect_transaction_block("e.request.wallet_address, &tx_bytes) .await - .map_err(|e| SwapperError::NetworkError(e.to_string()))?; + .map_err(|e| SwapperError::TransactionError(e.to_string()))?; let gas_budget = GasBudgetCalculator::gas_budget(&inspect_result.effects.gas_used); let coin_refs = all_coin_assets diff --git a/crates/swapper/src/chainlink.rs b/crates/swapper/src/chainlink.rs index d9df74cef..2a82a5595 100644 --- a/crates/swapper/src/chainlink.rs +++ b/crates/swapper/src/chainlink.rs @@ -24,8 +24,8 @@ impl ChainlinkPriceFeed { // Price is in 8 decimals pub fn decoded_answer(result: &IMulticall3::Result) -> Result { - let decoded = - decode_call3_return::(result).map_err(|_| SwapperError::ABIError("failed to decode answer".into()))?; + let decoded = decode_call3_return::(result) + .map_err(|_| SwapperError::ComputeQuoteError("failed to decode answer".into()))?; let price = BigInt::from_le_bytes(&decoded.answer.to_le_bytes::<32>()); Ok(price) } diff --git a/crates/swapper/src/error.rs b/crates/swapper/src/error.rs index 9a13c73fd..74d208ef0 100644 --- a/crates/swapper/src/error.rs +++ b/crates/swapper/src/error.rs @@ -1,126 +1,137 @@ use crate::alien::AlienError; use gem_client::ClientError; use gem_jsonrpc::types::JsonRpcError; +use serde::{Deserialize, Serialize}; use std::fmt::Debug; +use typeshare::typeshare; -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case", tag = "type", content = "message")] +#[typeshare(swift = "Equatable, Hashable, Sendable")] pub enum SwapperError { NotSupportedChain, NotSupportedAsset, - NotSupportedPair, NoAvailableProvider, - InvalidAddress(String), - InvalidAmount(String), - InputAmountTooSmall, + InputAmountError { min_amount: Option }, InvalidRoute, - NetworkError(String), - ABIError(String), ComputeQuoteError(String), TransactionError(String), NoQuoteAvailable, - NotImplemented, } impl std::fmt::Display for SwapperError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::NotSupportedChain => write!(f, "Not supported chain"), Self::NotSupportedAsset => write!(f, "Not supported asset"), - Self::NotSupportedPair => write!(f, "Not supported pair"), + Self::NotSupportedChain => write!(f, "Not supported chain"), Self::NoAvailableProvider => write!(f, "No available provider"), - Self::InvalidAddress(addr) => write!(f, "Invalid address {}", addr), - Self::InvalidAmount(amount) => write!(f, "Invalid amount {}", amount), - Self::InputAmountTooSmall => write!(f, "Input amount is too small"), + Self::InputAmountError { min_amount } => { + if let Some(min) = min_amount { + write!(f, "Input amount is too small (minimum {min})") + } else { + write!(f, "Input amount is too small") + } + } Self::InvalidRoute => write!(f, "Invalid route or route data"), - Self::NetworkError(msg) => write!(f, "Network related error: {}", msg), - Self::ABIError(msg) => write!(f, "ABI error: {}", msg), Self::ComputeQuoteError(msg) => write!(f, "Compute quote error: {}", msg), Self::TransactionError(msg) => write!(f, "Transaction error: {}", msg), Self::NoQuoteAvailable => write!(f, "No quote available"), - Self::NotImplemented => write!(f, "Not implemented"), } } } impl std::error::Error for SwapperError {} +impl SwapperError { + /// Standard messages for legacy invalid input errors that now map to compute/transaction failures. + pub const INVALID_AMOUNT_MESSAGE: &'static str = "Invalid amount"; + pub const INVALID_ADDRESS_MESSAGE: &'static str = "Invalid address"; + + pub fn into_transaction_error(self) -> Self { + match self { + SwapperError::ComputeQuoteError(msg) => SwapperError::TransactionError(msg), + other => other, + } + } +} + impl From for SwapperError { fn from(err: AlienError) -> Self { match err { - AlienError::RequestError { msg } => Self::NetworkError(format!("Alien request error: {msg}")), - AlienError::ResponseError { msg } => Self::NetworkError(format!("Alien response error: {msg}")), - AlienError::Http { status, len } => Self::NetworkError(format!("Alien HTTP error: status {}, body size: {}", status, len)), + AlienError::RequestError { msg } => Self::ComputeQuoteError(format!("Client request error: {msg}")), + AlienError::ResponseError { msg } => Self::ComputeQuoteError(format!("Client response error: {msg}")), + AlienError::Http { status, len } => Self::ComputeQuoteError(format!("Client HTTP error: status {}, body size: {}", status, len)), } } } impl From for SwapperError { fn from(err: JsonRpcError) -> Self { - Self::NetworkError(format!("JSON RPC error: {err}")) + Self::ComputeQuoteError(format!("JSON RPC error: {err}")) } } impl From for SwapperError { fn from(err: ClientError) -> Self { match err { - ClientError::Network(msg) => Self::NetworkError(msg), - ClientError::Timeout => Self::NetworkError("Request timed out".into()), - ClientError::Http { status, len } => Self::NetworkError(format!("HTTP error: status {}, body size: {}", status, len)), - ClientError::Serialization(msg) => Self::NetworkError(msg), + ClientError::Network(msg) => Self::ComputeQuoteError(msg), + ClientError::Timeout => Self::ComputeQuoteError("Request timed out".into()), + ClientError::Http { status, len } => Self::ComputeQuoteError(format!("HTTP error: status {}, body size: {}", status, len)), + ClientError::Serialization(msg) => Self::ComputeQuoteError(msg), } } } impl From for SwapperError { fn from(err: alloy_primitives::AddressError) -> Self { - Self::InvalidAddress(err.to_string()) + Self::ComputeQuoteError(format!("Invalid address: {err}")) } } impl From for SwapperError { fn from(err: sui_types::AddressParseError) -> Self { - Self::InvalidAddress(err.to_string()) + Self::ComputeQuoteError(format!("Invalid address: {err}")) } } impl From for SwapperError { fn from(err: serde_json::Error) -> Self { - Self::NetworkError(format!("serde_json::Error: {err}")) + Self::ComputeQuoteError(format!("serde_json::Error: {err}")) } } impl From for SwapperError { fn from(err: serde_urlencoded::ser::Error) -> Self { - Self::NetworkError(format!("Request query error: {err}")) + Self::ComputeQuoteError(format!("Request query error: {err}")) } } impl From for SwapperError { fn from(err: alloy_sol_types::Error) -> Self { - Self::ABIError(format!("AlloyError: {err}")) + Self::ComputeQuoteError(format!("AlloyError: {err}")) } } impl From for SwapperError { fn from(err: alloy_primitives::ruint::ParseError) -> Self { - Self::InvalidAmount(err.to_string()) + Self::ComputeQuoteError(format!("Invalid amount: {err}")) } } impl From for SwapperError { fn from(err: std::num::ParseIntError) -> Self { - Self::InvalidAmount(err.to_string()) + Self::ComputeQuoteError(format!("Invalid amount: {err}")) } } impl From for SwapperError { fn from(err: num_bigint::ParseBigIntError) -> Self { - Self::InvalidAmount(err.to_string()) + Self::ComputeQuoteError(format!("Invalid amount: {err}")) } } impl From for SwapperError { fn from(err: number_formatter::NumberFormatterError) -> Self { - Self::InvalidAmount(err.to_string()) + Self::ComputeQuoteError(format!("Invalid amount: {err}")) } } diff --git a/crates/swapper/src/eth_address.rs b/crates/swapper/src/eth_address.rs index c21144e98..0676ecdfe 100644 --- a/crates/swapper/src/eth_address.rs +++ b/crates/swapper/src/eth_address.rs @@ -25,10 +25,11 @@ pub(crate) fn parse_asset_id(asset: &AssetId) -> Result { if let Some(token_id) = &asset.token_id { parse_str(token_id) } else { - Err(SwapperError::InvalidAddress(asset.to_string())) + Err(SwapperError::ComputeQuoteError(format!("Invalid address {}", asset))) } } pub(crate) fn parse_str(str: &str) -> Result { - str.parse::
().map_err(|_| SwapperError::InvalidAddress(str.to_string())) + str.parse::
() + .map_err(|_| SwapperError::ComputeQuoteError(format!("Invalid address {}", str))) } diff --git a/crates/swapper/src/hyperliquid/provider/hyperliquid.rs b/crates/swapper/src/hyperliquid/provider/hyperliquid.rs index 12c951942..04c5fe5d1 100644 --- a/crates/swapper/src/hyperliquid/provider/hyperliquid.rs +++ b/crates/swapper/src/hyperliquid/provider/hyperliquid.rs @@ -66,7 +66,7 @@ impl Swapper for Hyperliquid { return self.bridge.fetch_quote(request).await; } - Err(SwapperError::NotSupportedPair) + Err(SwapperError::NotSupportedAsset) } async fn fetch_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { @@ -78,7 +78,7 @@ impl Swapper for Hyperliquid { return self.bridge.fetch_quote_data(quote, data).await; } - Err(SwapperError::NotSupportedPair) + Err(SwapperError::NotSupportedAsset) } async fn get_swap_result(&self, chain: primitives::Chain, transaction_hash: &str) -> Result { diff --git a/crates/swapper/src/hyperliquid/provider/math.rs b/crates/swapper/src/hyperliquid/provider/math.rs index b7e4127da..fce3c7900 100644 --- a/crates/swapper/src/hyperliquid/provider/math.rs +++ b/crates/swapper/src/hyperliquid/provider/math.rs @@ -19,7 +19,7 @@ pub fn scale_units(value: BigUint, from_decimals: u32, to_decimals: u32) -> Resu let factor = BigUint::from(10u32).pow(diff); let (quotient, remainder) = value.div_rem(&factor); if !remainder.is_zero() { - return Err(SwapperError::InvalidAmount("amount precision loss".to_string())); + return Err(SwapperError::ComputeQuoteError("amount precision loss".into())); } Ok(quotient) } @@ -55,7 +55,7 @@ mod tests { #[test] fn test_scale_units_precision_loss_rejected() { let err = scale_units(BigUint::from(5u32), 3, 1).unwrap_err(); - assert!(matches!(err, SwapperError::InvalidAmount(_))); + assert!(matches!(err, SwapperError::ComputeQuoteError(_))); } #[test] @@ -73,6 +73,6 @@ mod tests { #[test] fn test_scale_quote_value_invalid_number() { let err = scale_quote_value("abc", 6, 8).unwrap_err(); - assert!(matches!(err, SwapperError::InvalidAmount(_))); + assert!(matches!(err, SwapperError::ComputeQuoteError(_))); } } diff --git a/crates/swapper/src/hyperliquid/provider/spot/math.rs b/crates/swapper/src/hyperliquid/provider/spot/math.rs index bd7736c32..bb6dcd336 100644 --- a/crates/swapper/src/hyperliquid/provider/spot/math.rs +++ b/crates/swapper/src/hyperliquid/provider/spot/math.rs @@ -32,14 +32,14 @@ pub(super) fn format_decimal_with_scale(value: &BigDecimal, scale: u32) -> Strin pub(super) fn format_order_size(amount: &BigDecimal, decimals: u32) -> Result { let value = amount .to_f64() - .ok_or_else(|| SwapperError::InvalidAmount("failed to convert amount".to_string()))?; + .ok_or_else(|| SwapperError::ComputeQuoteError("failed to convert amount".into()))?; let rounded = round_to_decimals(value, decimals); let formatted = if decimals == 0 { format!("{rounded:.0}") } else { format!("{rounded:.decimals$}", decimals = decimals as usize) }; - let big_decimal = BigDecimal::from_str(&formatted).map_err(|_| SwapperError::InvalidAmount("failed to format size".to_string()))?; + let big_decimal = BigDecimal::from_str(&formatted).map_err(|_| SwapperError::ComputeQuoteError("failed to format size".into()))?; Ok(BigNumberFormatter::decimal_to_string(&big_decimal, decimals)) } @@ -49,18 +49,18 @@ pub(super) fn spot_asset_index(market_index: u32) -> u32 { pub(super) fn apply_slippage(limit_price: &BigDecimal, side: SpotSide, slippage_bps: u32, price_decimals: u32) -> Result { if limit_price <= &BigDecimal::zero() { - return Err(SwapperError::InvalidAmount("invalid limit price".to_string())); + return Err(SwapperError::ComputeQuoteError("invalid limit price".into())); } let limit_price_f64 = limit_price .to_f64() - .ok_or_else(|| SwapperError::InvalidAmount("failed to convert price".to_string()))?; + .ok_or_else(|| SwapperError::ComputeQuoteError("failed to convert price".into()))?; let slippage_fraction = slippage_bps as f64 / 10_000.0; let multiplier = if side.is_buy() { 1.0 + slippage_fraction } else { 1.0 - slippage_fraction }; if multiplier <= 0.0 { - return Err(SwapperError::InvalidAmount("slippage multiplier not positive".to_string())); + return Err(SwapperError::ComputeQuoteError("slippage multiplier not positive".into())); } let adjusted = limit_price_f64 * multiplier; @@ -72,7 +72,7 @@ pub(super) fn apply_slippage(limit_price: &BigDecimal, side: SpotSide, slippage_ format!("{rounded:.price_decimals$}", price_decimals = price_decimals as usize) }; - BigDecimal::from_str(&formatted).map_err(|_| SwapperError::InvalidAmount("failed to format limit price".to_string())) + BigDecimal::from_str(&formatted).map_err(|_| SwapperError::ComputeQuoteError("failed to format limit price".into())) } fn round_to_decimals(value: f64, decimals: u32) -> f64 { diff --git a/crates/swapper/src/hyperliquid/provider/spot/provider.rs b/crates/swapper/src/hyperliquid/provider/spot/provider.rs index 8549127f2..96c593c9c 100644 --- a/crates/swapper/src/hyperliquid/provider/spot/provider.rs +++ b/crates/swapper/src/hyperliquid/provider/spot/provider.rs @@ -57,12 +57,15 @@ impl HyperCoreSpot { async fn load_spot_meta(&self) -> Result { let client = self.client()?; - client.get_spot_meta().await.map_err(|err| SwapperError::NetworkError(err.to_string())) + client.get_spot_meta().await.map_err(|err| SwapperError::ComputeQuoteError(err.to_string())) } async fn load_orderbook(&self, coin: &str) -> Result { let client = self.client()?; - client.get_spot_orderbook(coin).await.map_err(|err| SwapperError::NetworkError(err.to_string())) + client + .get_spot_orderbook(coin) + .await + .map_err(|err| SwapperError::ComputeQuoteError(err.to_string())) } fn resolve_token<'a>(&self, meta: &'a SpotMeta, asset: &'a SwapperQuoteAsset) -> Result<&'a SpotToken, SwapperError> { @@ -100,7 +103,7 @@ impl HyperCoreSpot { meta.universe() .iter() .find(|market| market.tokens.len() == 2 && market.tokens[0] == base.index && market.tokens[1] == quote.index) - .ok_or(SwapperError::NotSupportedPair) + .ok_or(SwapperError::NotSupportedAsset) } } @@ -124,13 +127,13 @@ impl Swapper for HyperCoreSpot { let amount_in = BigNumberFormatter::big_decimal_value(&request.value, request.from_asset.decimals)?; if amount_in <= BigDecimal::zero() { - return Err(SwapperError::InvalidAmount("amount must be greater than zero".to_string())); + return Err(SwapperError::ComputeQuoteError("amount must be greater than zero".into())); } let (side, base_token, quote_token) = match (from_token.name.as_str(), to_token.name.as_str()) { (PAIR_BASE_SYMBOL, PAIR_QUOTE_SYMBOL) => (SpotSide::Sell, from_token, to_token), (PAIR_QUOTE_SYMBOL, PAIR_BASE_SYMBOL) => (SpotSide::Buy, to_token, from_token), - _ => return Err(SwapperError::NotSupportedPair), + _ => return Err(SwapperError::NotSupportedAsset), }; let market = self.resolve_market(&meta, base_token, quote_token)?; @@ -150,11 +153,11 @@ impl Swapper for HyperCoreSpot { let token_decimals: u32 = to_token .wei_decimals .try_into() - .map_err(|_| SwapperError::InvalidAmount("invalid amount precision".to_string()))?; + .map_err(|_| SwapperError::ComputeQuoteError("invalid amount precision".into()))?; let output_amount_str = format_decimal(&output_amount); let token_units = BigNumberFormatter::value_from_amount_biguint(&output_amount_str, token_decimals) - .map_err(|err| SwapperError::InvalidAmount(format!("invalid amount: {err}")))?; + .map_err(|err| SwapperError::ComputeQuoteError(format!("invalid amount: {err}")))?; let scaled_units = scale_units(token_units, token_decimals, request.to_asset.decimals)?; let to_value = scaled_units.to_string(); @@ -194,7 +197,7 @@ impl Swapper for HyperCoreSpot { async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let route = quote.data.routes.first().ok_or(SwapperError::InvalidRoute)?; let order: PlaceOrder = serde_json::from_str(&route.route_data).map_err(|_| SwapperError::InvalidRoute)?; - let order_json = serde_json::to_string(&order).map_err(|err| SwapperError::ComputeQuoteError(err.to_string()))?; + let order_json = serde_json::to_string(&order).map_err(|err| SwapperError::TransactionError(err.to_string()))?; Ok(SwapperQuoteData::new_contract( "".to_string(), diff --git a/crates/swapper/src/jupiter/provider.rs b/crates/swapper/src/jupiter/provider.rs index fa8ad18e3..f4440a434 100644 --- a/crates/swapper/src/jupiter/provider.rs +++ b/crates/swapper/src/jupiter/provider.rs @@ -48,7 +48,7 @@ where pub fn get_asset_address(&self, asset_id: &str) -> Result { get_pubkey_by_str(asset_id) .map(|x| x.to_string()) - .ok_or(SwapperError::InvalidAddress(asset_id.to_string())) + .ok_or_else(|| SwapperError::ComputeQuoteError(format!("Invalid address {asset_id}"))) } fn get_fee_mint(&self, mode: &SwapperMode, input: &str, output: &str) -> String { @@ -80,7 +80,7 @@ where value .value .map(|x| x.owner) - .ok_or(SwapperError::NetworkError("fetch_token_program error".to_string())) + .ok_or_else(|| SwapperError::ComputeQuoteError("fetch_token_program error".to_string())) } async fn fetch_fee_account(&self, mode: &SwapperMode, options: &Options, input_mint: &str, output_mint: &str) -> Result { diff --git a/crates/swapper/src/near_intents/provider.rs b/crates/swapper/src/near_intents/provider.rs index 0e03f0deb..99638e7e2 100644 --- a/crates/swapper/src/near_intents/provider.rs +++ b/crates/swapper/src/near_intents/provider.rs @@ -91,7 +91,9 @@ where let amount_u256 = Self::parse_u256(&base_amount, "amount")?; if amount_u256 <= reserved_fee { - return Err(SwapperError::InputAmountTooSmall); + return Err(SwapperError::InputAmountError { + min_amount: Some(reserved_fee.to_string()), + }); } Ok((amount_u256 - reserved_fee).to_string()) @@ -229,7 +231,7 @@ where from_asset.asset_id().token_id.as_deref(), ) .await - .map_err(|err| SwapperError::NetworkError(format!("Failed to build Sui deposit data: {err}")))?; + .map_err(|err| SwapperError::TransactionError(format!("Failed to build Sui deposit data: {err}")))?; Ok(DepositData { to: deposit_address.to_string(), @@ -250,9 +252,9 @@ where fn map_quote_error(error: &QuoteResponseError) -> SwapperError { let lower = error.message.to_ascii_lowercase(); if lower.contains("too low") { - SwapperError::InputAmountTooSmall + SwapperError::InputAmountError { min_amount: None } } else { - SwapperError::NetworkError(format!("Near Intents quote error: {}", error.message)) + SwapperError::ComputeQuoteError(format!("Near Intents quote error: {}", error.message)) } } @@ -272,7 +274,7 @@ where async fn fetch_quote(&self, request: &QuoteRequest) -> Result { let mode = match request.mode { SwapperMode::ExactIn => SwapType::FlexInput, - SwapperMode::ExactOut => return Err(SwapperError::NotImplemented), + SwapperMode::ExactOut => todo!("ExactOut mode is not supported for Near Intents"), }; let amount = Self::resolve_quote_amount(request, &mode)?; @@ -432,7 +434,7 @@ mod tests { let err = NearIntents::::resolve_quote_amount(&request, &SwapType::FlexInput).expect_err("expected error"); - assert!(matches!(err, SwapperError::InputAmountTooSmall)); + assert!(matches!(err, SwapperError::InputAmountError { .. })); } #[test] @@ -446,7 +448,7 @@ mod tests { match decoded { QuoteResponseResult::Err(err) => { assert_eq!(err.message, "Amount is too low for bridge, try at least 8516130"); - assert!(matches!(map_quote_error(&err), SwapperError::InputAmountTooSmall)); + assert!(matches!(map_quote_error(&err), SwapperError::InputAmountError { .. })); } QuoteResponseResult::Ok(_) => panic!("expected error variant"), } @@ -521,12 +523,12 @@ mod swap_integration_tests { let quote = match provider.fetch_quote(&request).await { Ok(quote) => quote, - Err(SwapperError::NetworkError(_)) => return Ok(()), + Err(SwapperError::ComputeQuoteError(_)) => return Ok(()), Err(error) => return Err(error), }; let quote_data = match provider.fetch_quote_data("e, FetchQuoteData::None).await { Ok(data) => data, - Err(SwapperError::NetworkError(_)) => return Ok(()), + Err(SwapperError::TransactionError(_)) => return Ok(()), Err(error) => return Err(error), }; diff --git a/crates/swapper/src/permit2_data.rs b/crates/swapper/src/permit2_data.rs index 580bb4f9d..cc5cfcc5f 100644 --- a/crates/swapper/src/permit2_data.rs +++ b/crates/swapper/src/permit2_data.rs @@ -96,7 +96,7 @@ pub fn permit2_data_to_eip712_json(chain: Chain, data: PermitSingle, contract: & primary_type: "PermitSingle".into(), message: data, }; - let json = serde_json::to_string(&message).map_err(|_| SwapperError::ABIError("failed to serialize EIP712 message to JSON".into()))?; + let json = serde_json::to_string(&message).map_err(|_| SwapperError::TransactionError("failed to serialize EIP712 message to JSON".into()))?; Ok(json) } diff --git a/crates/swapper/src/proxy/client.rs b/crates/swapper/src/proxy/client.rs index 7f44e2af0..517beeaa0 100644 --- a/crates/swapper/src/proxy/client.rs +++ b/crates/swapper/src/proxy/client.rs @@ -1,14 +1,21 @@ use crate::SwapperError; -use gem_client::Client; +use gem_client::{Client, build_path_with_query}; use primitives::swap::{ProxyQuote, ProxyQuoteRequest, SwapQuoteData}; use serde::Deserialize; +use serde::Serialize; use std::fmt::Debug; +const API_VERSION: u8 = 1; + #[derive(Debug, Deserialize)] -#[serde(untagged)] enum ProxyResult { - Ok(T), - Err { error: String }, + Ok { ok: T }, + Err { err: ProxyError }, +} + +#[derive(Debug, Deserialize)] +enum ProxyError { + Object { code: SwapperError }, } #[derive(Clone, Debug)] @@ -28,18 +35,32 @@ where } pub async fn get_quote(&self, request: ProxyQuoteRequest) -> Result { - let response: ProxyResult = self.client.post("/quote", &request, None).await.map_err(SwapperError::from)?; + let path = build_path_with_query("/quote", &VersionQuery { v: API_VERSION }).map_err(SwapperError::from)?; + let response: ProxyResult = self.client.post(&path, &request, None).await.map_err(SwapperError::from)?; match response { - ProxyResult::Ok(q) => Ok(q), - ProxyResult::Err { error } => Err(SwapperError::ComputeQuoteError(error)), + ProxyResult::Ok { ok } => Ok(ok), + ProxyResult::Err { err } => Err(map_proxy_error(err)), } } pub async fn get_quote_data(&self, quote: ProxyQuote) -> Result { - let response: ProxyResult = self.client.post("/quote_data", "e, None).await.map_err(SwapperError::from)?; + let path = build_path_with_query("/quote_data", &VersionQuery { v: API_VERSION }).map_err(SwapperError::from)?; + let response: ProxyResult = self.client.post(&path, "e, None).await.map_err(SwapperError::from)?; match response { - ProxyResult::Ok(qd) => Ok(qd), - ProxyResult::Err { error } => Err(SwapperError::TransactionError(error)), + ProxyResult::Ok { ok } => Ok(ok), + ProxyResult::Err { err } => Err(map_proxy_error(err)), } } } + +#[derive(Debug, Serialize)] +struct VersionQuery { + v: u8, +} + +/// Try to cast a proxy error back into a `SwapperError` variant. +fn map_proxy_error(error: ProxyError) -> SwapperError { + match error { + ProxyError::Object { code } => code, + } +} diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index 40478e974..4ec881d96 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -258,7 +258,7 @@ where if self.provider.mode == SwapperProviderMode::OnChain { Ok(self.get_onchain_swap_status(chain, transaction_hash)) } else { - Err(SwapperError::NotImplemented) + todo!("Swap result not implemented for provider {:?}", self.provider.id) } } } diff --git a/crates/swapper/src/swapper.rs b/crates/swapper/src/swapper.rs index dfddffe01..e59c10f52 100644 --- a/crates/swapper/src/swapper.rs +++ b/crates/swapper/src/swapper.rs @@ -75,6 +75,18 @@ impl GemSwapper { from_symbol.contains("USD") && to_symbol.contains("USD") } + + fn prioritized_error(errors: &[SwapperError]) -> Option { + for err in errors { + if let SwapperError::InputAmountError { min_amount } = err { + return Some(SwapperError::InputAmountError { + min_amount: min_amount.clone(), + }); + } + } + + None + } } impl GemSwapper { @@ -137,7 +149,7 @@ impl GemSwapper { pub async fn fetch_quote(&self, request: &QuoteRequest) -> Result, SwapperError> { if request.from_asset.id == request.to_asset.id { - return Err(SwapperError::NotSupportedPair); + return Err(SwapperError::NotSupportedAsset); } let from_chain = request.from_asset.chain(); let to_chain = request.to_asset.chain(); @@ -157,21 +169,25 @@ impl GemSwapper { let request_for_quote = Self::transform_request(request); let quotes_futures = providers.into_iter().map(|x| x.fetch_quote(request_for_quote.as_ref())); - let quotes = futures::future::join_all(quotes_futures.into_iter().map(|fut| async { - match &fut.await { - Ok(quote) => Some(quote.clone()), - Err(_err) => { - tracing::debug!("fetch_quote error: {:?}", _err); - None + let quote_results = futures::future::join_all(quotes_futures).await; + + let mut quotes = Vec::new(); + let mut errors = Vec::new(); + + for result in quote_results { + match result { + Ok(quote) => quotes.push(quote), + Err(err) => { + tracing::debug!("fetch_quote error: {:?}", err); + errors.push(err); } } - })) - .await - .into_iter() - .flatten() - .collect::>(); + } if quotes.is_empty() { + if let Some(error) = Self::prioritized_error(&errors) { + return Err(error); + } return Err(SwapperError::NoQuoteAvailable); } @@ -207,15 +223,18 @@ impl GemSwapper { #[cfg(all(test, feature = "reqwest_provider"))] mod tests { + use std::{borrow::Cow, collections::BTreeSet, sync::Arc, vec}; + + use primitives::{AssetId, Chain, asset_constants::USDT_ETH_ASSET_ID}; + use super::*; use crate::{ - Options, SwapperMode, SwapperQuoteAsset, SwapperSlippage, SwapperSlippageMode, + Options, SwapperChainAsset, SwapperMode, SwapperProvider, SwapperQuoteAsset, SwapperSlippage, SwapperSlippageMode, alien::reqwest_provider::NativeProvider, config::{DEFAULT_STABLE_SWAP_REFERRAL_BPS, DEFAULT_SWAP_FEE_BPS, ReferralFees}, + testkit::{MockSwapper, mock_quote}, uniswap::default::{new_pancakeswap, new_uniswap_v3}, }; - use primitives::asset_constants::USDT_ETH_ASSET_ID; - use std::{borrow::Cow, collections::BTreeSet, sync::Arc, vec}; fn build_request(from_symbol: &str, to_symbol: &str, fee: Option) -> QuoteRequest { QuoteRequest { @@ -385,4 +404,55 @@ mod tests { let stable_without_fees = build_request("USDC", "USDT", None); assert!(matches!(GemSwapper::transform_request(&stable_without_fees), Cow::Borrowed(_))); } + + #[tokio::test] + async fn test_fetch_quote_amount_error() { + let request = mock_quote( + SwapperQuoteAsset::from(AssetId::from_chain(Chain::Ethereum)), + SwapperQuoteAsset::from(AssetId::from_token(Chain::Ethereum, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")), + ); + + let gem_swapper = GemSwapper { + rpc_provider: Arc::new(NativeProvider::default()), + swappers: vec![ + Box::new(MockSwapper::new(SwapperProvider::UniswapV3, || { + Err(SwapperError::InputAmountError { min_amount: None }) + })), + Box::new(MockSwapper::new(SwapperProvider::PancakeswapV3, || { + Err(SwapperError::InputAmountError { min_amount: None }) + })), + Box::new(MockSwapper::new(SwapperProvider::Jupiter, || Err(SwapperError::NoQuoteAvailable))), + ], + }; + + let result = gem_swapper.fetch_quote(&request).await; + + match result { + Err(SwapperError::InputAmountError { min_amount: _ }) => {} + _ => panic!("expected InputAmountError when every provider rejects the amount"), + } + } + + #[tokio::test] + async fn test_fetch_quote_no_quote() { + let request = mock_quote( + SwapperQuoteAsset::from(AssetId::from_chain(Chain::Ethereum)), + SwapperQuoteAsset::from(AssetId::from_token(Chain::Ethereum, "0xdAC17F958D2ee523a2206206994597C13D831ec7")), + ); + + let gem_swapper = GemSwapper { + rpc_provider: Arc::new(NativeProvider::default()), + swappers: vec![ + Box::new(MockSwapper::new(SwapperProvider::UniswapV3, || Err(SwapperError::NoQuoteAvailable))), + Box::new(MockSwapper::new(SwapperProvider::PancakeswapV3, || Err(SwapperError::NoQuoteAvailable))), + ], + }; + + let result = gem_swapper.fetch_quote(&request).await; + + match result { + Err(SwapperError::NoQuoteAvailable) => {} + _ => panic!("expected NoQuoteAvailable when providers failed without amount errors"), + } + } } diff --git a/crates/swapper/src/swapper_trait.rs b/crates/swapper/src/swapper_trait.rs index d67bc0fa4..2aa9eedb4 100644 --- a/crates/swapper/src/swapper_trait.rs +++ b/crates/swapper/src/swapper_trait.rs @@ -21,7 +21,7 @@ pub trait Swapper: Send + Sync + Debug { if self.provider().mode == SwapperProviderMode::OnChain { Ok(self.get_onchain_swap_status(chain, transaction_hash)) } else { - Err(SwapperError::NotImplemented) + todo!("get_swap_result not implemented for provider {:?}", self.provider().id) } } diff --git a/crates/swapper/src/testkit.rs b/crates/swapper/src/testkit.rs index 5ea9adc7e..282b1480e 100644 --- a/crates/swapper/src/testkit.rs +++ b/crates/swapper/src/testkit.rs @@ -1,6 +1,11 @@ -use crate::{SwapperQuoteAsset, SwapperSlippage, SwapperSlippageMode, config::get_swap_config}; +use crate::{ + FetchQuoteData, ProviderType, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, SwapperSlippage, + SwapperSlippageMode, config::get_swap_config, +}; +use async_trait::async_trait; +use primitives::Chain; -use super::{Options, QuoteRequest, SwapperMode}; +use super::{Options, Quote, QuoteRequest, SwapperMode}; pub fn mock_quote(from_asset: SwapperQuoteAsset, to_asset: SwapperQuoteAsset) -> QuoteRequest { let config = get_swap_config(); @@ -23,3 +28,41 @@ pub fn mock_quote(from_asset: SwapperQuoteAsset, to_asset: SwapperQuoteAsset) -> }, } } + +type MockResponse = fn() -> Result; + +#[derive(Debug)] +pub struct MockSwapper { + provider: ProviderType, + supported_assets: Vec, + response: MockResponse, +} + +impl MockSwapper { + pub fn new(provider: SwapperProvider, response: MockResponse) -> Self { + Self { + provider: ProviderType::new(provider), + supported_assets: vec![SwapperChainAsset::All(Chain::Ethereum)], + response, + } + } +} + +#[async_trait] +impl Swapper for MockSwapper { + fn provider(&self) -> &ProviderType { + &self.provider + } + + fn supported_assets(&self) -> Vec { + self.supported_assets.clone() + } + + async fn fetch_quote(&self, _request: &QuoteRequest) -> Result { + (self.response)() + } + + async fn fetch_quote_data(&self, _quote: &Quote, _data: FetchQuoteData) -> Result { + todo!("MockSwapper fetch_quote_data not implemented") + } +} diff --git a/crates/swapper/src/thorchain/provider.rs b/crates/swapper/src/thorchain/provider.rs index d0931bca6..1aa8bc601 100644 --- a/crates/swapper/src/thorchain/provider.rs +++ b/crates/swapper/src/thorchain/provider.rs @@ -73,7 +73,9 @@ where .ok_or(SwapperError::InvalidRoute)?; if from_inbound_address.dust_threshold > value { - return Err(SwapperError::InputAmountTooSmall); + return Err(SwapperError::InputAmountError { + min_amount: Some(from_inbound_address.dust_threshold.to_string()), + }); } } diff --git a/crates/swapper/src/uniswap/v3/path.rs b/crates/swapper/src/uniswap/v3/path.rs index a5ea08ef1..3b03e3de1 100644 --- a/crates/swapper/src/uniswap/v3/path.rs +++ b/crates/swapper/src/uniswap/v3/path.rs @@ -43,7 +43,7 @@ pub fn build_paths_with_routes(routes: &[Route]) -> Result return Err(SwapperError::InvalidRoute); } let route_data: RouteData = serde_json::from_str(&routes[0].route_data).map_err(|_| SwapperError::InvalidRoute)?; - let fee_tier = FeeTier::try_from(route_data.fee_tier.as_str()).map_err(|_| SwapperError::InvalidAmount("invalid fee tier".into()))?; + let fee_tier = FeeTier::try_from(route_data.fee_tier.as_str()).map_err(|_| SwapperError::ComputeQuoteError("invalid fee tier".into()))?; let token_pairs: Vec = routes .iter() .map(|route| TokenPair { diff --git a/crates/swapper/src/uniswap/v3/quoter_v2.rs b/crates/swapper/src/uniswap/v3/quoter_v2.rs index 5d1677396..2db00d6c0 100644 --- a/crates/swapper/src/uniswap/v3/quoter_v2.rs +++ b/crates/swapper/src/uniswap/v3/quoter_v2.rs @@ -30,7 +30,7 @@ pub fn build_quoter_request(mode: &SwapperMode, wallet_address: &str, quoter_v2: // Returns (amountOut, gasEstimate) pub fn decode_quoter_response(response: &JsonRpcResponse) -> Result<(U256, U256), SwapperError> { - let decoded = HexDecode(&response.result).map_err(|_| SwapperError::NetworkError("Failed to decode quoter response".into()))?; + let decoded = HexDecode(&response.result).map_err(|_| SwapperError::ComputeQuoteError("Failed to decode quoter response".into()))?; let quoter_return = IQuoterV2::quoteExactInputCall::abi_decode_returns(&decoded).map_err(SwapperError::from)?; Ok((quoter_return.amountOut, quoter_return.gasEstimate)) diff --git a/crates/swapper/src/uniswap/v4/path.rs b/crates/swapper/src/uniswap/v4/path.rs index 53931ae79..0707bcc6f 100644 --- a/crates/swapper/src/uniswap/v4/path.rs +++ b/crates/swapper/src/uniswap/v4/path.rs @@ -91,11 +91,15 @@ impl TryFrom<&Route> for PathKey { type Error = SwapperError; fn try_from(value: &Route) -> Result { - let token_id = value.output.token_id.as_ref().ok_or(SwapperError::InvalidAddress(value.output.to_string()))?; + let token_id = value + .output + .token_id + .as_ref() + .ok_or_else(|| SwapperError::ComputeQuoteError(format!("Invalid address {}", value.output)))?; let currency = eth_address::parse_str(token_id)?; let route_data: RouteData = serde_json::from_str(&value.route_data).map_err(|_| SwapperError::InvalidRoute)?; - let fee_tier = FeeTier::try_from(route_data.fee_tier.as_str()).map_err(|_| SwapperError::InvalidAmount("invalid fee tier".into()))?; + let fee_tier = FeeTier::try_from(route_data.fee_tier.as_str()).map_err(|_| SwapperError::ComputeQuoteError("invalid fee tier".into()))?; Ok(PathKey { intermediateCurrency: currency, fee: fee_tier.as_u24(), diff --git a/crates/swapper/src/uniswap/v4/quoter.rs b/crates/swapper/src/uniswap/v4/quoter.rs index 53505f5e4..dca1e59d7 100644 --- a/crates/swapper/src/uniswap/v4/quoter.rs +++ b/crates/swapper/src/uniswap/v4/quoter.rs @@ -43,7 +43,7 @@ pub fn build_quote_exact_request(v4_quoter: &str, params: &IV4Quoter::QuoteExact // Returns (amountOut, gasEstimate) pub fn decode_quoter_response(response: &JsonRpcResponse) -> Result<(U256, U256), SwapperError> { - let decoded = HexDecode(&response.result).map_err(|e| SwapperError::NetworkError(e.to_string()))?; + let decoded = HexDecode(&response.result).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?; let quoter_return = IV4Quoter::quoteExactInputSingleCall::abi_decode_returns(&decoded).map_err(SwapperError::from)?; Ok((quoter_return.amountOut, quoter_return.gasEstimate)) diff --git a/gemstone/src/gem_swapper/error.rs b/gemstone/src/gem_swapper/error.rs index 43a03e773..f022fad22 100644 --- a/gemstone/src/gem_swapper/error.rs +++ b/gemstone/src/gem_swapper/error.rs @@ -4,16 +4,10 @@ pub type SwapperError = swapper::SwapperError; pub enum SwapperError { NotSupportedChain, NotSupportedAsset, - NotSupportedPair, NoAvailableProvider, - InvalidAddress(String), - InvalidAmount(String), - InputAmountTooSmall, + InputAmountError { min_amount: Option }, InvalidRoute, - NetworkError(String), - ABIError(String), ComputeQuoteError(String), TransactionError(String), NoQuoteAvailable, - NotImplemented, }