From 7ea9596138b8c9d8d17e862f40e6bc31eaa01475 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:56:36 +0900 Subject: [PATCH 1/7] Respect provider non approval gas_limit for solana swaps Parse and honor provider-specified gas limits for Solana swap transactions and ensure approval-derived gas is merged with route data. - preload_mapper: for TransactionInputType::Swap read SwapData.data.gas_limit (parse as u64) and use it instead of the hardcoded 420_000 fallback. Added mock_swap_data_with_gas_limit helper and updated/added tests to cover provider gas_limit handling and to use Asset::mock_* helpers. - swapper proxy/provider: renamed approval gas var to approval_gas_limit and combine it with route data gas_limit (approval takes precedence if present) when building SwapperQuoteData. These changes allow swap providers to specify custom gas limits and ensure the final quote includes gas limits from both approval checks and route data. --- .../gem_solana/src/provider/preload_mapper.rs | 77 ++++++++----------- crates/swapper/src/proxy/provider.rs | 3 +- 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/crates/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index e2e1c0080..80dce708d 100644 --- a/crates/gem_solana/src/provider/preload_mapper.rs +++ b/crates/gem_solana/src/provider/preload_mapper.rs @@ -41,7 +41,12 @@ fn get_gas_limit(input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) => BigInt::from(100_000), - TransactionInputType::Swap(_, _, _) => BigInt::from(420_000), + TransactionInputType::Swap(_, _, swap_data) => { + swap_data.data.gas_limit.as_ref() + .and_then(|gl| gl.parse::().ok()) + .map(BigInt::from) + .unwrap_or(BigInt::from(420_000)) + } TransactionInputType::Stake(_, _) => BigInt::from(100_000), } } @@ -112,9 +117,14 @@ mod tests { use primitives::swap::SwapData; use primitives::{Asset, AssetId, AssetType, Chain, SwapProvider}; + fn mock_swap_data_with_gas_limit(provider: SwapProvider, gas_limit: Option<&str>) -> SwapData { + let mut data = SwapData::mock_with_provider(provider); + data.data.gas_limit = gas_limit.map(|s| s.to_string()); + data + } + #[test] fn test_calculate_transaction_fee() { - // Test with EIP-1559 gas pricing let gas_price_type = GasPriceType::eip1559(BigInt::from(5000u64), BigInt::from(15000u64)); let input_type = TransactionInputType::Transfer(Asset { id: AssetId::from_chain(Chain::Solana), @@ -137,36 +147,33 @@ mod tests { #[test] fn test_calculate_transaction_fee_swap() { - // Test swap transaction with higher gas limit - let gas_price_type = GasPriceType::eip1559(BigInt::from(5000u64), BigInt::from(30000u64)); + let gas_price_type = GasPriceType::solana(5000u64, 30000u64, 100u64); let input_type = TransactionInputType::Swap( - Asset { - id: AssetId::from_chain(Chain::Solana), - chain: Chain::Solana, - token_id: None, - name: "SOL".to_string(), - symbol: "SOL".to_string(), - decimals: 9, - asset_type: AssetType::NATIVE, - }, - Asset { - id: AssetId::from_chain(Chain::Solana), - chain: Chain::Solana, - token_id: Some("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string()), - name: "USDC".to_string(), - symbol: "USDC".to_string(), - decimals: 6, - asset_type: AssetType::SPL, - }, - SwapData::mock_with_provider(SwapProvider::Jupiter), + Asset::mock_sol(), + Asset::mock_spl_token(), + mock_swap_data_with_gas_limit(SwapProvider::Jupiter, None), ); - let fee = calculate_transaction_fee(&input_type, &gas_price_type, None); + let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("existing_account".to_string())); assert_eq!(fee.fee, BigInt::from(35_000u64)); assert_eq!(fee.gas_limit, BigInt::from(420_000u64)); } + #[test] + fn test_calculate_transaction_fee_swap_with_provider_gas_limit() { + let gas_price_type = GasPriceType::solana(5000u64, 30000u64, 100u64); + let input_type = TransactionInputType::Swap( + Asset::mock_sol(), + Asset::mock_spl_token(), + mock_swap_data_with_gas_limit(SwapProvider::Okx, Some("550000")), + ); + + let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("existing_account".to_string())); + + assert_eq!(fee.gas_limit, BigInt::from(550_000u64)); + } + #[test] fn test_calculate_transaction_fee_cross_chain_swap_without_token_creation() { let gas_price_type = GasPriceType::eip1559(BigInt::from(5000u64), BigInt::from(15000u64)); @@ -274,25 +281,9 @@ mod tests { fn test_calculate_fee_rates_swap() { let fees = vec![SolanaPrioritizationFee { prioritization_fee: 150_000 }]; let input_type = TransactionInputType::Swap( - Asset { - id: AssetId::from_chain(Chain::Solana), - chain: Chain::Solana, - token_id: None, - name: "SOL".to_string(), - symbol: "SOL".to_string(), - decimals: 9, - asset_type: AssetType::NATIVE, - }, - Asset { - id: AssetId::from_chain(Chain::Solana), - chain: Chain::Solana, - token_id: Some("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string()), - name: "USDC".to_string(), - symbol: "USDC".to_string(), - decimals: 6, - asset_type: AssetType::SPL, - }, - SwapData::mock_with_provider(SwapProvider::Jupiter), + Asset::mock_sol(), + Asset::mock_spl_token(), + mock_swap_data_with_gas_limit(SwapProvider::Jupiter, None), ); let rates = calculate_fee_rates(&input_type, &fees); diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index c5c07f330..ba01e1ec1 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -222,7 +222,8 @@ where let route_data: ProxyQuote = serde_json::from_str(&routes.first().unwrap().route_data).map_err(|_| SwapperError::InvalidRoute)?; let data = self.client.get_quote_data(route_data).await?; - let (approval, gas_limit) = self.check_approval(quote, &data).await?; + let (approval, approval_gas_limit) = self.check_approval(quote, &data).await?; + let gas_limit = approval_gas_limit.or(data.gas_limit); Ok(SwapperQuoteData::new_contract(data.to, data.value, data.data, approval, gas_limit)) } From 2555c47aa5708c22dc84a1eb8df88033d55c4c79 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:28:54 +0900 Subject: [PATCH 2/7] Preserve provider gas_limit and add tests Add support to preserve provider-specified gas_limit for non-EVM chains and update related code and tests. Introduces SwapQuoteData::mock_with_gas_limit helper, renames check_approval to check_approval_and_limit and returns the provider gas limit for non-EVM chains (previously always None). Update call site to use the new method and propagate gas_limit into SwapperQuoteData. Add MockClient and several testkit helpers plus unit tests verifying Solana preserves provider gas limits and EVM-native chains ignore them. Also add a dev-dependency on primitives with the testkit feature to Cargo.toml. --- crates/primitives/src/testkit/swap_mock.rs | 7 +++ crates/swapper/Cargo.toml | 1 + crates/swapper/src/proxy/provider.rs | 56 +++++++++++++++++-- crates/swapper/src/testkit.rs | 63 +++++++++++++++++++++- 4 files changed, 120 insertions(+), 7 deletions(-) diff --git a/crates/primitives/src/testkit/swap_mock.rs b/crates/primitives/src/testkit/swap_mock.rs index fb5660204..539a596b5 100644 --- a/crates/primitives/src/testkit/swap_mock.rs +++ b/crates/primitives/src/testkit/swap_mock.rs @@ -59,6 +59,13 @@ impl SwapQuoteData { gas_limit: Some("21000".to_string()), } } + + pub fn mock_with_gas_limit(gas_limit: Option) -> Self { + SwapQuoteData { + gas_limit, + ..Self::mock() + } + } } impl SwapProviderData { diff --git a/crates/swapper/Cargo.toml b/crates/swapper/Cargo.toml index 8124b5fc0..e3275d547 100644 --- a/crates/swapper/Cargo.toml +++ b/crates/swapper/Cargo.toml @@ -50,3 +50,4 @@ tracing = "0.1.44" [dev-dependencies] tokio.workspace = true +primitives = { path = "../primitives", features = ["testkit"], default-features = false } diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index ba01e1ec1..3bb5ce55c 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -46,7 +46,7 @@ where } } - pub async fn check_approval(&self, quote: &Quote, quote_data: &SwapQuoteData) -> Result<(Option, Option), SwapperError> { + pub async fn check_approval_and_limit(&self, quote: &Quote, quote_data: &SwapQuoteData) -> Result<(Option, Option), SwapperError> { let request = "e.request; let from_asset = request.from_asset.asset_id(); @@ -66,8 +66,7 @@ where .await } } - ChainType::Tron => Ok((None, None)), - _ => Ok((None, None)), + _ => Ok((None, quote_data.gas_limit.clone())), } } @@ -222,8 +221,7 @@ where let route_data: ProxyQuote = serde_json::from_str(&routes.first().unwrap().route_data).map_err(|_| SwapperError::InvalidRoute)?; let data = self.client.get_quote_data(route_data).await?; - let (approval, approval_gas_limit) = self.check_approval(quote, &data).await?; - let gas_limit = approval_gas_limit.or(data.gas_limit); + let (approval, gas_limit) = self.check_approval_and_limit(quote, &data).await?; Ok(SwapperQuoteData::new_contract(data.to, data.value, data.data, approval, gas_limit)) } @@ -262,6 +260,54 @@ where } } +#[cfg(test)] +mod tests { + use super::*; + use crate::testkit::MockClient; + use primitives::swap::SwapQuoteData; + + fn mock_provider(provider: SwapperProvider) -> ProxyProvider { + let rpc_provider = Arc::new(crate::alien::mock::ProviderMock::new("{}".to_string())); + ProxyProvider::new_with_client(provider, super::super::client::ProxyClient::new(MockClient), vec![], rpc_provider) + } + + #[tokio::test] + async fn test_solana_preserves_provider_gas_limit() { + let provider = mock_provider(SwapperProvider::Okx); + let quote = Quote::mock(Chain::Solana, None); + let data = SwapQuoteData::mock_with_gas_limit(Some("550000".to_string())); + + let (approval, gas_limit) = provider.check_approval_and_limit("e, &data).await.unwrap(); + + assert!(approval.is_none()); + assert_eq!(gas_limit, Some("550000".to_string())); + } + + #[tokio::test] + async fn test_solana_returns_none_when_no_provider_gas_limit() { + let provider = mock_provider(SwapperProvider::Okx); + let quote = Quote::mock(Chain::Solana, None); + let data = SwapQuoteData::mock_with_gas_limit(None); + + let (approval, gas_limit) = provider.check_approval_and_limit("e, &data).await.unwrap(); + + assert!(approval.is_none()); + assert!(gas_limit.is_none()); + } + + #[tokio::test] + async fn test_evm_native_ignores_provider_gas_limit() { + let provider = mock_provider(SwapperProvider::Mayan); + let quote = Quote::mock(Chain::Ethereum, None); + let data = SwapQuoteData::mock_with_gas_limit(Some("550000".to_string())); + + let (approval, gas_limit) = provider.check_approval_and_limit("e, &data).await.unwrap(); + + assert!(approval.is_none()); + assert!(gas_limit.is_none()); + } +} + #[cfg(all(test, feature = "swap_integration_tests"))] mod swap_integration_tests { use super::*; diff --git a/crates/swapper/src/testkit.rs b/crates/swapper/src/testkit.rs index 1763b2290..e3f15a4b4 100644 --- a/crates/swapper/src/testkit.rs +++ b/crates/swapper/src/testkit.rs @@ -1,12 +1,71 @@ use crate::{ - FetchQuoteData, ProviderType, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, SwapperSlippage, SwapperSlippageMode, + FetchQuoteData, ProviderData, ProviderType, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, SwapperSlippage, SwapperSlippageMode, config::get_swap_config, }; use async_trait::async_trait; -use primitives::Chain; +use gem_client::Client; +use primitives::{AssetId, Chain}; +use serde::{Serialize, de::DeserializeOwned}; +use std::collections::HashMap; use super::{Options, Quote, QuoteRequest, SwapperMode}; +#[derive(Debug, Clone)] +pub struct MockClient; + +#[async_trait] +impl Client for MockClient { + async fn get(&self, _path: &str) -> Result + where + R: DeserializeOwned, + { + unimplemented!() + } + async fn post(&self, _path: &str, _body: &T, _headers: Option>) -> Result + where + T: Serialize + Send + Sync, + R: DeserializeOwned, + { + unimplemented!() + } +} + +impl ProviderData { + pub fn mock() -> Self { + ProviderData { + provider: ProviderType::new(SwapperProvider::Okx), + routes: vec![], + slippage_bps: 50, + } + } +} + +impl QuoteRequest { + pub fn mock(chain: Chain, token_id: Option<&str>) -> Self { + QuoteRequest { + from_asset: SwapperQuoteAsset::from(AssetId::from(chain, token_id.map(|s| s.to_string()))), + to_asset: SwapperQuoteAsset::from(AssetId::from_chain(chain)), + wallet_address: "address".to_string(), + destination_address: "address".to_string(), + value: "1000000".to_string(), + mode: SwapperMode::ExactIn, + options: Options::default(), + } + } +} + +impl Quote { + pub fn mock(chain: Chain, token_id: Option<&str>) -> Self { + Quote { + from_value: "1000000".to_string(), + to_value: "1000000".to_string(), + data: ProviderData::mock(), + request: QuoteRequest::mock(chain, token_id), + eta_in_seconds: None, + } + } +} + pub fn mock_quote(from_asset: SwapperQuoteAsset, to_asset: SwapperQuoteAsset) -> QuoteRequest { let config = get_swap_config(); From d554a4b98b8aad73d03c3994b4881d845146a285 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:50:26 +0900 Subject: [PATCH 3/7] format code --- .../gem_solana/src/provider/preload_mapper.rs | 31 ++++++------------- gemstone/src/models/transaction.rs | 4 +-- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/crates/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index 80dce708d..6e4eb839c 100644 --- a/crates/gem_solana/src/provider/preload_mapper.rs +++ b/crates/gem_solana/src/provider/preload_mapper.rs @@ -41,12 +41,13 @@ fn get_gas_limit(input_type: &TransactionInputType) -> BigInt { | TransactionInputType::TokenApprove(_, _) | TransactionInputType::Generic(_, _, _) | TransactionInputType::Perpetual(_, _) => BigInt::from(100_000), - TransactionInputType::Swap(_, _, swap_data) => { - swap_data.data.gas_limit.as_ref() - .and_then(|gl| gl.parse::().ok()) - .map(BigInt::from) - .unwrap_or(BigInt::from(420_000)) - } + TransactionInputType::Swap(_, _, swap_data) => swap_data + .data + .gas_limit + .as_ref() + .and_then(|x| x.parse::().ok()) + .map(BigInt::from) + .unwrap_or(BigInt::from(420_000)), TransactionInputType::Stake(_, _) => BigInt::from(100_000), } } @@ -148,11 +149,7 @@ mod tests { #[test] fn test_calculate_transaction_fee_swap() { let gas_price_type = GasPriceType::solana(5000u64, 30000u64, 100u64); - let input_type = TransactionInputType::Swap( - Asset::mock_sol(), - Asset::mock_spl_token(), - mock_swap_data_with_gas_limit(SwapProvider::Jupiter, None), - ); + let input_type = TransactionInputType::Swap(Asset::mock_sol(), Asset::mock_spl_token(), mock_swap_data_with_gas_limit(SwapProvider::Jupiter, None)); let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("existing_account".to_string())); @@ -163,11 +160,7 @@ mod tests { #[test] fn test_calculate_transaction_fee_swap_with_provider_gas_limit() { let gas_price_type = GasPriceType::solana(5000u64, 30000u64, 100u64); - let input_type = TransactionInputType::Swap( - Asset::mock_sol(), - Asset::mock_spl_token(), - mock_swap_data_with_gas_limit(SwapProvider::Okx, Some("550000")), - ); + let input_type = TransactionInputType::Swap(Asset::mock_sol(), Asset::mock_spl_token(), mock_swap_data_with_gas_limit(SwapProvider::Okx, Some("550000"))); let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("existing_account".to_string())); @@ -280,11 +273,7 @@ mod tests { #[test] fn test_calculate_fee_rates_swap() { let fees = vec![SolanaPrioritizationFee { prioritization_fee: 150_000 }]; - let input_type = TransactionInputType::Swap( - Asset::mock_sol(), - Asset::mock_spl_token(), - mock_swap_data_with_gas_limit(SwapProvider::Jupiter, None), - ); + let input_type = TransactionInputType::Swap(Asset::mock_sol(), Asset::mock_spl_token(), mock_swap_data_with_gas_limit(SwapProvider::Jupiter, None)); let rates = calculate_fee_rates(&input_type, &fees); assert_eq!(rates.len(), 3); diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 40fb4f9c9..450b59e9d 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -730,8 +730,8 @@ impl From for GemTransferDataExtra { fn from(value: TransferDataExtra) -> Self { GemTransferDataExtra { to: value.to, - gas_limit: value.gas_limit.map(|gl| gl.to_string()), - gas_price: value.gas_price.map(|gp| gp.into()), + gas_limit: value.gas_limit.map(|x| x.to_string()), + gas_price: value.gas_price.map(|x| x.into()), data: value.data, output_type: value.output_type, output_action: value.output_action, From 09b57bf60fb00474228470711e87bedceb74ba1c Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:39:33 +0900 Subject: [PATCH 4/7] code cleanup --- crates/gem_solana/src/provider/preload_mapper.rs | 6 +++--- crates/swapper/src/proxy/provider.rs | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/gem_solana/src/provider/preload_mapper.rs b/crates/gem_solana/src/provider/preload_mapper.rs index 6e4eb839c..9cff291fa 100644 --- a/crates/gem_solana/src/provider/preload_mapper.rs +++ b/crates/gem_solana/src/provider/preload_mapper.rs @@ -151,7 +151,7 @@ mod tests { let gas_price_type = GasPriceType::solana(5000u64, 30000u64, 100u64); let input_type = TransactionInputType::Swap(Asset::mock_sol(), Asset::mock_spl_token(), mock_swap_data_with_gas_limit(SwapProvider::Jupiter, None)); - let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("existing_account".to_string())); + let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("recipient_token_address".to_string())); assert_eq!(fee.fee, BigInt::from(35_000u64)); assert_eq!(fee.gas_limit, BigInt::from(420_000u64)); @@ -162,7 +162,7 @@ mod tests { let gas_price_type = GasPriceType::solana(5000u64, 30000u64, 100u64); let input_type = TransactionInputType::Swap(Asset::mock_sol(), Asset::mock_spl_token(), mock_swap_data_with_gas_limit(SwapProvider::Okx, Some("550000"))); - let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("existing_account".to_string())); + let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("recipient_token_address".to_string())); assert_eq!(fee.gas_limit, BigInt::from(550_000u64)); } @@ -345,7 +345,7 @@ mod tests { }; let input_type = TransactionInputType::Transfer(asset); - let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("existing_account".to_string())); + let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("recipient_token_address".to_string())); assert_eq!(fee.fee, BigInt::from(20_000u64)); assert!(fee.options.is_empty()); diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index 3bb5ce55c..46eeef656 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -262,13 +262,15 @@ where #[cfg(test)] mod tests { + use super::super::client::ProxyClient; use super::*; + use crate::alien::mock::ProviderMock; use crate::testkit::MockClient; use primitives::swap::SwapQuoteData; fn mock_provider(provider: SwapperProvider) -> ProxyProvider { - let rpc_provider = Arc::new(crate::alien::mock::ProviderMock::new("{}".to_string())); - ProxyProvider::new_with_client(provider, super::super::client::ProxyClient::new(MockClient), vec![], rpc_provider) + let rpc_provider = Arc::new(ProviderMock::new("{}".to_string())); + ProxyProvider::new_with_client(provider, ProxyClient::new(MockClient), vec![], rpc_provider) } #[tokio::test] From 98e0dc78b89751ce0e9f1362bae8c7ff6be492f0 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:41:56 +0900 Subject: [PATCH 5/7] merge solana gas limit test --- crates/swapper/src/proxy/provider.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index 46eeef656..1356a36fe 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -283,12 +283,7 @@ mod tests { assert!(approval.is_none()); assert_eq!(gas_limit, Some("550000".to_string())); - } - #[tokio::test] - async fn test_solana_returns_none_when_no_provider_gas_limit() { - let provider = mock_provider(SwapperProvider::Okx); - let quote = Quote::mock(Chain::Solana, None); let data = SwapQuoteData::mock_with_gas_limit(None); let (approval, gas_limit) = provider.check_approval_and_limit("e, &data).await.unwrap(); From 009ca843cb4f3442e0e555878b12b23af4936b9a Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:54:18 +0900 Subject: [PATCH 6/7] move MockClient to gem_client/testkit --- crates/gem_client/Cargo.toml | 1 + crates/gem_client/src/lib.rs | 9 ++-- crates/gem_client/src/reqwest_client.rs | 6 +-- crates/gem_client/src/retry.rs | 2 +- crates/gem_client/src/testkit.rs | 64 +++++++++++++++++++++++++ crates/swapper/Cargo.toml | 1 + crates/swapper/src/proxy/provider.rs | 4 +- crates/swapper/src/testkit.rs | 27 +---------- 8 files changed, 80 insertions(+), 34 deletions(-) create mode 100644 crates/gem_client/src/testkit.rs diff --git a/crates/gem_client/Cargo.toml b/crates/gem_client/Cargo.toml index f75e75668..7649fcc99 100644 --- a/crates/gem_client/Cargo.toml +++ b/crates/gem_client/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [features] default = [] reqwest = ["dep:reqwest", "dep:tokio"] +testkit = [] [dependencies] async-trait = { workspace = true } diff --git a/crates/gem_client/src/lib.rs b/crates/gem_client/src/lib.rs index 7456f24d4..4fa8125d4 100644 --- a/crates/gem_client/src/lib.rs +++ b/crates/gem_client/src/lib.rs @@ -1,6 +1,9 @@ mod content_type; mod types; +#[cfg(feature = "testkit")] +pub mod testkit; + #[cfg(feature = "reqwest")] mod reqwest_client; @@ -12,9 +15,9 @@ pub mod client_config; pub mod query; -pub use content_type::{ContentType, CONTENT_TYPE}; +pub use content_type::{CONTENT_TYPE, ContentType}; pub use query::build_path_with_query; -pub use types::{decode_json_byte_array, deserialize_response, ClientError, Response}; +pub use types::{ClientError, Response, decode_json_byte_array, deserialize_response}; #[cfg(feature = "reqwest")] pub use reqwest_client::ReqwestClient; @@ -26,7 +29,7 @@ pub use retry::{default_should_retry, retry, retry_policy}; pub use client_config::builder; use async_trait::async_trait; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use std::{collections::HashMap, fmt::Debug}; pub type Data = Vec; diff --git a/crates/gem_client/src/reqwest_client.rs b/crates/gem_client/src/reqwest_client.rs index 74e5e13a3..339c93e95 100644 --- a/crates/gem_client/src/reqwest_client.rs +++ b/crates/gem_client/src/reqwest_client.rs @@ -1,8 +1,8 @@ -use crate::{deserialize_response, retry_policy, Client, ClientError, ContentType, Response, CONTENT_TYPE}; +use crate::{CONTENT_TYPE, Client, ClientError, ContentType, Response, deserialize_response, retry_policy}; use async_trait::async_trait; -use reqwest::header::USER_AGENT; use reqwest::RequestBuilder; -use serde::{de::DeserializeOwned, Serialize}; +use reqwest::header::USER_AGENT; +use serde::{Serialize, de::DeserializeOwned}; use std::{collections::HashMap, str::FromStr, time::Duration}; #[derive(Debug, Clone)] diff --git a/crates/gem_client/src/retry.rs b/crates/gem_client/src/retry.rs index 20c384551..d7ebe7ca7 100644 --- a/crates/gem_client/src/retry.rs +++ b/crates/gem_client/src/retry.rs @@ -1,4 +1,4 @@ -use reqwest::{retry, StatusCode}; +use reqwest::{StatusCode, retry}; use std::future::Future; use std::time::Duration; diff --git a/crates/gem_client/src/testkit.rs b/crates/gem_client/src/testkit.rs new file mode 100644 index 000000000..a2b99d3bf --- /dev/null +++ b/crates/gem_client/src/testkit.rs @@ -0,0 +1,64 @@ +use crate::{Client, ClientError}; +use async_trait::async_trait; +use serde::{Serialize, de::DeserializeOwned}; +use std::{collections::HashMap, sync::Arc}; + +type GetHandler = Arc Result, ClientError> + Send + Sync>; +type PostHandler = Arc Result, ClientError> + Send + Sync>; + +#[derive(Clone, Default)] +pub struct MockClient { + get_handler: Option, + post_handler: Option, +} + +impl MockClient { + pub fn new() -> Self { + Self::default() + } + + pub fn with_get(mut self, handler: F) -> Self + where + F: Fn(&str) -> Result, ClientError> + Send + Sync + 'static, + { + self.get_handler = Some(Arc::new(handler)); + self + } + + pub fn with_post(mut self, handler: F) -> Self + where + F: Fn(&str, &[u8]) -> Result, ClientError> + Send + Sync + 'static, + { + self.post_handler = Some(Arc::new(handler)); + self + } +} + +impl std::fmt::Debug for MockClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MockClient").finish() + } +} + +#[async_trait] +impl Client for MockClient { + async fn get(&self, path: &str) -> Result + where + R: DeserializeOwned, + { + let handler = self.get_handler.as_ref().expect("MockClient: get handler not set"); + let bytes = handler(path)?; + serde_json::from_slice(&bytes).map_err(|e| ClientError::Serialization(e.to_string())) + } + + async fn post(&self, path: &str, body: &T, _headers: Option>) -> Result + where + T: Serialize + Send + Sync, + R: DeserializeOwned, + { + let handler = self.post_handler.as_ref().expect("MockClient: post handler not set"); + let body_bytes = serde_json::to_vec(body).map_err(|e| ClientError::Serialization(e.to_string()))?; + let bytes = handler(path, &body_bytes)?; + serde_json::from_slice(&bytes).map_err(|e| ClientError::Serialization(e.to_string())) + } +} diff --git a/crates/swapper/Cargo.toml b/crates/swapper/Cargo.toml index e3275d547..3315296d3 100644 --- a/crates/swapper/Cargo.toml +++ b/crates/swapper/Cargo.toml @@ -51,3 +51,4 @@ tracing = "0.1.44" [dev-dependencies] tokio.workspace = true primitives = { path = "../primitives", features = ["testkit"], default-features = false } +gem_client = { path = "../gem_client", features = ["testkit"] } diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index 1356a36fe..01135463c 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -265,12 +265,12 @@ mod tests { use super::super::client::ProxyClient; use super::*; use crate::alien::mock::ProviderMock; - use crate::testkit::MockClient; + use gem_client::testkit::MockClient; use primitives::swap::SwapQuoteData; fn mock_provider(provider: SwapperProvider) -> ProxyProvider { let rpc_provider = Arc::new(ProviderMock::new("{}".to_string())); - ProxyProvider::new_with_client(provider, ProxyClient::new(MockClient), vec![], rpc_provider) + ProxyProvider::new_with_client(provider, ProxyClient::new(MockClient::new()), vec![], rpc_provider) } #[tokio::test] diff --git a/crates/swapper/src/testkit.rs b/crates/swapper/src/testkit.rs index e3f15a4b4..83b0cca1f 100644 --- a/crates/swapper/src/testkit.rs +++ b/crates/swapper/src/testkit.rs @@ -1,35 +1,12 @@ use crate::{ - FetchQuoteData, ProviderData, ProviderType, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, SwapperSlippage, SwapperSlippageMode, - config::get_swap_config, + FetchQuoteData, ProviderData, ProviderType, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, SwapperSlippage, + SwapperSlippageMode, config::get_swap_config, }; use async_trait::async_trait; -use gem_client::Client; use primitives::{AssetId, Chain}; -use serde::{Serialize, de::DeserializeOwned}; -use std::collections::HashMap; use super::{Options, Quote, QuoteRequest, SwapperMode}; -#[derive(Debug, Clone)] -pub struct MockClient; - -#[async_trait] -impl Client for MockClient { - async fn get(&self, _path: &str) -> Result - where - R: DeserializeOwned, - { - unimplemented!() - } - async fn post(&self, _path: &str, _body: &T, _headers: Option>) -> Result - where - T: Serialize + Send + Sync, - R: DeserializeOwned, - { - unimplemented!() - } -} - impl ProviderData { pub fn mock() -> Self { ProviderData { From 5d41b698404aced14f6b8b74d1d3614e862413d5 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:04:58 +0900 Subject: [PATCH 7/7] Update testkit.rs --- crates/gem_client/src/testkit.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/gem_client/src/testkit.rs b/crates/gem_client/src/testkit.rs index a2b99d3bf..e81805aae 100644 --- a/crates/gem_client/src/testkit.rs +++ b/crates/gem_client/src/testkit.rs @@ -1,7 +1,11 @@ use crate::{Client, ClientError}; use async_trait::async_trait; -use serde::{Serialize, de::DeserializeOwned}; -use std::{collections::HashMap, sync::Arc}; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + collections::HashMap, + fmt::{Debug, Formatter}, + sync::Arc, +}; type GetHandler = Arc Result, ClientError> + Send + Sync>; type PostHandler = Arc Result, ClientError> + Send + Sync>; @@ -34,8 +38,8 @@ impl MockClient { } } -impl std::fmt::Debug for MockClient { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Debug for MockClient { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("MockClient").finish() } } @@ -46,7 +50,7 @@ impl Client for MockClient { where R: DeserializeOwned, { - let handler = self.get_handler.as_ref().expect("MockClient: get handler not set"); + let handler = self.get_handler.as_ref().ok_or(ClientError::Http { status: 404, body: vec![] })?; let bytes = handler(path)?; serde_json::from_slice(&bytes).map_err(|e| ClientError::Serialization(e.to_string())) } @@ -56,7 +60,7 @@ impl Client for MockClient { T: Serialize + Send + Sync, R: DeserializeOwned, { - let handler = self.post_handler.as_ref().expect("MockClient: post handler not set"); + let handler = self.post_handler.as_ref().ok_or(ClientError::Http { status: 404, body: vec![] })?; let body_bytes = serde_json::to_vec(body).map_err(|e| ClientError::Serialization(e.to_string()))?; let bytes = handler(path, &body_bytes)?; serde_json::from_slice(&bytes).map_err(|e| ClientError::Serialization(e.to_string()))