From fc33c220a3ad4f0a620638b60bb8fc5c6669b4ae Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:00:05 +0900 Subject: [PATCH 01/20] Add Relay swap provider and client for evm chains --- crates/swapper/src/models.rs | 2 +- crates/swapper/src/near_intents/provider.rs | 1 - crates/swapper/src/proxy/provider.rs | 28 +-- crates/swapper/src/proxy/provider_factory.rs | 4 - crates/swapper/src/relay/asset.rs | 96 +++++++++- crates/swapper/src/relay/chain.rs | 44 ++++- crates/swapper/src/relay/client.rs | 37 ++++ crates/swapper/src/relay/mapper.rs | 107 ++++++++++- crates/swapper/src/relay/mod.rs | 36 +++- crates/swapper/src/relay/model.rs | 123 +++++++++++- crates/swapper/src/relay/provider.rs | 191 +++++++++++++++++++ crates/swapper/src/relay/testkit.rs | 27 ++- crates/swapper/src/swapper.rs | 6 +- 13 files changed, 652 insertions(+), 50 deletions(-) create mode 100644 crates/swapper/src/relay/client.rs create mode 100644 crates/swapper/src/relay/provider.rs diff --git a/crates/swapper/src/models.rs b/crates/swapper/src/models.rs index a63410f8c..d93a9be5e 100644 --- a/crates/swapper/src/models.rs +++ b/crates/swapper/src/models.rs @@ -47,7 +47,7 @@ impl ProviderType { | SwapperProvider::Okx => SwapProviderMode::OnChain, SwapperProvider::Mayan | SwapperProvider::Chainflip | SwapperProvider::NearIntents => SwapProviderMode::CrossChain, SwapperProvider::Thorchain => SwapProviderMode::OmniChain(vec![Chain::Thorchain, Chain::Tron]), - SwapperProvider::Relay => SwapProviderMode::OmniChain(vec![Chain::Hyperliquid, Chain::Manta, Chain::Berachain]), + SwapperProvider::Relay => SwapProviderMode::OmniChain(vec![Chain::Hyperliquid, Chain::Berachain]), SwapperProvider::Across => SwapProviderMode::Bridge, SwapperProvider::Hyperliquid => SwapProviderMode::OmniChain(vec![Chain::HyperCore, Chain::Hyperliquid]), } diff --git a/crates/swapper/src/near_intents/provider.rs b/crates/swapper/src/near_intents/provider.rs index 6b87b4c00..96e9e09ac 100644 --- a/crates/swapper/src/near_intents/provider.rs +++ b/crates/swapper/src/near_intents/provider.rs @@ -552,7 +552,6 @@ mod swap_integration_tests { use primitives::{ AssetId, Chain, asset_constants::{USDC_ARB_ASSET_ID, USDC_BASE_ASSET_ID}, - swap::SwapStatus, }; use std::sync::Arc; diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index b61cedb6e..7fd8642fa 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -13,9 +13,8 @@ use crate::{ asset::*, config::{DEFAULT_SWAP_FEE_BPS, get_swap_api_url}, models::{ApprovalType, SwapperChainAsset}, - relay, }; -use gem_client::{Client, ClientExt}; +use gem_client::Client; use primitives::{ Chain, ChainType, TransactionSwapMetadata, swap::{ApprovalData, ProxyQuote, ProxyQuoteRequest, SwapQuoteData}, @@ -171,15 +170,6 @@ impl ProxyProvider { Self::new_with_path(SwapperProvider::Mayan, "mayan", assets, rpc_provider) } - - pub fn new_relay(rpc_provider: Arc) -> Self { - Self::new_with_path( - SwapperProvider::Relay, - "relay", - vec![SwapperChainAsset::All(Chain::Hyperliquid), SwapperChainAsset::All(Chain::Berachain)], - rpc_provider, - ) - } } #[async_trait] @@ -267,14 +257,6 @@ where Ok(SwapResult { status, metadata }) } - SwapperProvider::Relay => { - let base_url = get_swap_api_url("relay"); - let client = RpcClient::new(base_url, self.rpc_provider.clone()); - let path = format!("/requests/v2?hash={}", transaction_hash); - let response: relay::model::RelayRequestsResponse = ClientExt::get(&client, &path).await.map_err(SwapperError::from)?; - let request = response.requests.first().ok_or(SwapperError::InvalidRoute)?; - Ok(relay::map_swap_result(request)) - } _ => { if self.provider.mode == SwapperProviderMode::OnChain { Ok(SwapResult { @@ -290,12 +272,6 @@ where async fn get_vault_addresses(&self, _from_timestamp: Option) -> Result, SwapperError> { match self.provider.id { - SwapperProvider::Relay => { - let base_url = get_swap_api_url("relay"); - let client = RpcClient::new(base_url, self.rpc_provider.clone()); - let chains: relay::model::RelayChainsResponse = ClientExt::get(&client, "/chains").await.map_err(SwapperError::from)?; - Ok(chains.solver_addresses()) - } SwapperProvider::Mayan => { let static_addresses: BTreeSet = MAYAN_CONTRACTS.iter().map(|s| s.to_string()).collect(); let base_url = get_swap_api_url("mayan/price"); @@ -361,7 +337,7 @@ mod swap_integration_tests { alien::reqwest_provider::NativeProvider, {SwapperMode, SwapperQuoteAsset, asset::SUI_USDC_TOKEN_ID, models::Options}, }; - use primitives::{AssetId, TransactionSwapMetadata, swap::SwapStatus}; + use primitives::{AssetId, swap::SwapStatus}; #[tokio::test] async fn test_mayan_provider_fetch_quote() -> Result<(), SwapperError> { diff --git a/crates/swapper/src/proxy/provider_factory.rs b/crates/swapper/src/proxy/provider_factory.rs index 6d672eb41..3114dad8c 100644 --- a/crates/swapper/src/proxy/provider_factory.rs +++ b/crates/swapper/src/proxy/provider_factory.rs @@ -26,7 +26,3 @@ pub fn new_panora(rpc_provider: Arc) -> ProxyProvider) -> ProxyProvider { ProxyProvider::new_mayan(rpc_provider) } - -pub fn new_relay(rpc_provider: Arc) -> ProxyProvider { - ProxyProvider::new_relay(rpc_provider) -} diff --git a/crates/swapper/src/relay/asset.rs b/crates/swapper/src/relay/asset.rs index 6ea125798..ed1c7e3ef 100644 --- a/crates/swapper/src/relay/asset.rs +++ b/crates/swapper/src/relay/asset.rs @@ -1,8 +1,17 @@ +use std::sync::LazyLock; + use gem_evm::address::ethereum_address_checksum; use gem_solana::{SYSTEM_PROGRAM_ID, WSOL_TOKEN_ADDRESS}; -use primitives::{AssetId, Chain, ChainType}; +use primitives::{ + AssetId, Chain, ChainType, + asset_constants::{ + USDC_ARB_ASSET_ID, USDC_HYPEREVM_ASSET_ID, USDC_OP_ASSET_ID, USDC_POLYGON_ASSET_ID, USDT_ARB_ASSET_ID, USDT_HYPEREVM_ASSET_ID, USDT_LINEA_ASSET_ID, USDT_OP_ASSET_ID, + USDT_POLYGON_ASSET_ID, USDT_ZKSYNC_ASSET_ID, + }, +}; -use crate::asset::EVM_ZERO_ADDRESS; +use super::chain::RelayChain; +use crate::{SwapperChainAsset, SwapperError, asset::*}; fn is_native_currency(chain: Chain, currency: &str) -> bool { match chain { @@ -24,3 +33,86 @@ pub fn map_currency_to_asset_id(chain: Chain, currency: &str) -> AssetId { } AssetId::from_token(chain, currency) } + +pub static SUPPORTED_CHAINS: LazyLock> = LazyLock::new(|| { + vec![ + SwapperChainAsset::Assets( + Chain::Ethereum, + vec![ + AssetId::from_chain(Chain::Ethereum), + AssetId::from_token(Chain::Ethereum, ETHEREUM_USDC_TOKEN_ID), + AssetId::from_token(Chain::Ethereum, ETHEREUM_USDT_TOKEN_ID), + ], + ), + SwapperChainAsset::Assets( + Chain::SmartChain, + vec![ + AssetId::from_chain(Chain::SmartChain), + AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDC_TOKEN_ID), + AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDT_TOKEN_ID), + ], + ), + SwapperChainAsset::Assets(Chain::Base, vec![AssetId::from_chain(Chain::Base), AssetId::from_token(Chain::Base, BASE_USDC_TOKEN_ID)]), + SwapperChainAsset::Assets( + Chain::Arbitrum, + vec![AssetId::from_chain(Chain::Arbitrum), USDC_ARB_ASSET_ID.into(), USDT_ARB_ASSET_ID.into()], + ), + SwapperChainAsset::Assets( + Chain::Optimism, + vec![AssetId::from_chain(Chain::Optimism), USDC_OP_ASSET_ID.into(), USDT_OP_ASSET_ID.into()], + ), + SwapperChainAsset::Assets( + Chain::Polygon, + vec![AssetId::from_chain(Chain::Polygon), USDC_POLYGON_ASSET_ID.into(), USDT_POLYGON_ASSET_ID.into()], + ), + SwapperChainAsset::Assets( + Chain::AvalancheC, + vec![ + AssetId::from_chain(Chain::AvalancheC), + AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDC_TOKEN_ID), + AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDT_TOKEN_ID), + ], + ), + SwapperChainAsset::Assets(Chain::Linea, vec![AssetId::from_chain(Chain::Linea), USDT_LINEA_ASSET_ID.into()]), + SwapperChainAsset::Assets(Chain::ZkSync, vec![AssetId::from_chain(Chain::ZkSync), USDT_ZKSYNC_ASSET_ID.into()]), + SwapperChainAsset::Assets( + Chain::Hyperliquid, + vec![AssetId::from_chain(Chain::Hyperliquid), USDC_HYPEREVM_ASSET_ID.into(), USDT_HYPEREVM_ASSET_ID.into()], + ), + SwapperChainAsset::Assets(Chain::Berachain, vec![]), + SwapperChainAsset::Assets(Chain::Abstract, vec![]), + SwapperChainAsset::Assets(Chain::Mantle, vec![]), + SwapperChainAsset::Assets(Chain::Celo, vec![]), + SwapperChainAsset::Assets(Chain::Stable, vec![]), + ] +}); + +pub fn asset_to_currency(asset_id: &AssetId, relay_chain: &RelayChain) -> Result { + if !relay_chain.is_evm() { + return Err(SwapperError::NotSupportedChain); + } + if asset_id.is_native() { + Ok(EVM_ZERO_ADDRESS.to_string()) + } else { + asset_id.token_id.clone().ok_or(SwapperError::NotSupportedAsset) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use primitives::Chain; + + #[test] + fn test_evm_native_asset() { + let result = asset_to_currency(&AssetId::from_chain(Chain::Ethereum), &RelayChain::from_chain(&Chain::Ethereum).unwrap()).unwrap(); + assert_eq!(result, EVM_ZERO_ADDRESS); + } + + #[test] + fn test_evm_token_asset() { + let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + let result = asset_to_currency(&AssetId::from_token(Chain::Ethereum, token_address), &RelayChain::from_chain(&Chain::Ethereum).unwrap()).unwrap(); + assert_eq!(result, token_address); + } +} diff --git a/crates/swapper/src/relay/chain.rs b/crates/swapper/src/relay/chain.rs index 0864da532..a6c32c8cf 100644 --- a/crates/swapper/src/relay/chain.rs +++ b/crates/swapper/src/relay/chain.rs @@ -10,6 +10,30 @@ pub enum RelayChain { } impl RelayChain { + pub fn chain_id(&self) -> u64 { + match self { + Self::Bitcoin => BITCOIN_CHAIN_ID, + Self::Solana => SOLANA_CHAIN_ID, + Self::Evm(evm_chain) => evm_chain.chain_id(), + } + } + + pub fn from_chain(chain: &Chain) -> Option { + match chain { + Chain::Bitcoin => Some(Self::Bitcoin), + Chain::Solana => Some(Self::Solana), + _ => Some(Self::Evm(EVMChain::from_chain(*chain)?)), + } + } + + pub fn to_chain(&self) -> Chain { + match self { + Self::Bitcoin => Chain::Bitcoin, + Self::Solana => Chain::Solana, + Self::Evm(evm_chain) => evm_chain.to_chain(), + } + } + pub fn from_chain_id(chain_id: u64) -> Option { match chain_id { BITCOIN_CHAIN_ID => Some(Self::Bitcoin), @@ -21,11 +45,23 @@ impl RelayChain { } } - pub fn to_chain(&self) -> Chain { + pub fn is_evm(&self) -> bool { match self { - Self::Bitcoin => Chain::Bitcoin, - Self::Solana => Chain::Solana, - Self::Evm(evm_chain) => evm_chain.to_chain(), + Self::Evm(_) => true, + Self::Bitcoin | Self::Solana => false, } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_chain() { + assert_eq!(RelayChain::from_chain(&Chain::Ethereum).unwrap().chain_id(), EVMChain::Ethereum.chain_id()); + assert_eq!(RelayChain::from_chain(&Chain::SmartChain).unwrap().chain_id(), EVMChain::SmartChain.chain_id()); + assert_eq!(RelayChain::from_chain(&Chain::Solana).unwrap().chain_id(), SOLANA_CHAIN_ID); + assert!(RelayChain::from_chain(&Chain::Cosmos).is_none()); + } +} diff --git a/crates/swapper/src/relay/client.rs b/crates/swapper/src/relay/client.rs new file mode 100644 index 000000000..939c24444 --- /dev/null +++ b/crates/swapper/src/relay/client.rs @@ -0,0 +1,37 @@ +use std::{collections::HashMap, fmt::Debug}; + +use gem_client::{CONTENT_TYPE, Client, ClientExt, ContentType}; + +use super::model::{RelayChainsResponse, RelayQuoteRequest, RelayQuoteResponse, RelayRequestsResponse}; +use crate::SwapperError; + +#[derive(Clone, Debug)] +pub struct RelayClient +where + C: Client + Clone + Send + Sync + Debug + 'static, +{ + client: C, +} + +impl RelayClient +where + C: Client + Clone + Send + Sync + Debug + 'static, +{ + pub fn new(client: C) -> Self { + Self { client } + } + + pub async fn get_quote(&self, request: RelayQuoteRequest) -> Result { + let headers = HashMap::from([(CONTENT_TYPE.to_string(), ContentType::ApplicationJson.as_str().into())]); + self.client.post_with("/quote/v2", &request, headers).await.map_err(SwapperError::from) + } + + pub async fn get_request(&self, transaction_hash: &str) -> Result { + let path = format!("/requests/v2?hash={}", transaction_hash); + self.client.get(&path).await.map_err(SwapperError::from) + } + + pub async fn get_chains(&self) -> Result { + self.client.get("/chains").await.map_err(SwapperError::from) + } +} diff --git a/crates/swapper/src/relay/mapper.rs b/crates/swapper/src/relay/mapper.rs index 39c7d52b6..140b25047 100644 --- a/crates/swapper/src/relay/mapper.rs +++ b/crates/swapper/src/relay/mapper.rs @@ -1,7 +1,50 @@ -use primitives::TransactionSwapMetadata; +use primitives::{TransactionSwapMetadata, swap::ApprovalData}; -use super::{asset::map_currency_to_asset_id, chain::RelayChain, model::RelayRequest}; -use crate::{SwapResult, SwapperProvider}; +use super::{ + DEFAULT_GAS_LIMIT, + asset::map_currency_to_asset_id, + chain::RelayChain, + model::{RelayFees, RelayRequest, Step, StepData}, +}; +use crate::{SwapResult, SwapperError, SwapperProvider, SwapperQuoteData}; + +pub const STEP_SWAP: &str = "swap"; +pub const STEP_DEPOSIT: &str = "deposit"; +pub const STEP_APPROVE: &str = "approve"; + +pub fn get_step_data(steps: &[Step]) -> Result<&StepData, SwapperError> { + steps + .iter() + .find(|s| s.id == STEP_SWAP || s.id == STEP_DEPOSIT) + .or_else(|| steps.iter().find(|s| s.kind == "transaction" && s.id != STEP_APPROVE)) + .or_else(|| steps.iter().find(|s| s.step_data().is_some())) + .and_then(|s| s.step_data()) + .ok_or(SwapperError::InvalidRoute) +} + +pub fn gas_fee_amount(fees: &Option) -> Option { + fees.as_ref()?.gas.as_ref()?.amount.clone() +} + +pub fn map_quote_data(chain: &RelayChain, steps: &[Step], value: &str, fees: &Option, approval: Option) -> Result { + let step_data = get_step_data(steps)?; + + let (to, tx_value, data, gas_limit) = match chain { + RelayChain::Bitcoin => { + let psbt = step_data.psbt.as_ref().ok_or(SwapperError::InvalidRoute)?; + (String::new(), value.to_string(), psbt.clone(), gas_fee_amount(fees)) + } + _ if chain.is_evm() => { + let to = step_data.to.clone().unwrap_or_default(); + let data = step_data.data.clone().unwrap_or_default(); + let gas_limit = approval.as_ref().map(|_| DEFAULT_GAS_LIMIT.to_string()); + (to, step_data.value.clone(), data, gas_limit) + } + _ => return Err(SwapperError::NotSupportedChain), + }; + + Ok(SwapperQuoteData::new_contract(to, tx_value, data, approval, gas_limit)) +} pub fn map_swap_result(request: &RelayRequest) -> SwapResult { let metadata = request.data.as_ref().and_then(|d| d.metadata.as_ref()).and_then(|m| { @@ -27,9 +70,40 @@ pub fn map_swap_result(request: &RelayRequest) -> SwapResult { #[cfg(test)] mod tests { use super::*; - use crate::relay::model::{RelayCurrencyDetail, RelayRequest, RelayRequestMetadata, RelayRequestsResponse, RelayStatus}; + use crate::relay::model::{RelayCurrencyDetail, RelayRequest, RelayRequestMetadata, RelayRequestsResponse, RelayStatus, Step}; use primitives::{AssetId, Chain, swap::SwapStatus}; + #[test] + fn test_map_evm_quote_data() { + let steps = vec![Step::mock_transaction("swap", "0xrouter", "1000000000000000000", "0xabcdef")]; + let chain = RelayChain::from_chain(&Chain::Ethereum).unwrap(); + + let result = map_quote_data(&chain, &steps, "1000000000000000000", &None, None).unwrap(); + + assert_eq!(result.to, "0xrouter"); + assert_eq!(result.value, "1000000000000000000"); + assert_eq!(result.data, "0xabcdef"); + assert!(result.approval.is_none()); + assert!(result.gas_limit.is_none()); + } + + #[test] + fn test_map_evm_quote_data_with_approval() { + let steps = vec![Step::mock_transaction("swap", "0xrouter", "0", "0xabcdef")]; + let chain = RelayChain::from_chain(&Chain::Ethereum).unwrap(); + let approval = ApprovalData { + token: "0xtoken".to_string(), + spender: "0xrouter".to_string(), + value: "1000".to_string(), + }; + + let result = map_quote_data(&chain, &steps, "1000000000000000000", &None, Some(approval.clone())).unwrap(); + + assert_eq!(result.to, "0xrouter"); + assert_eq!(result.approval, Some(approval)); + assert_eq!(result.gas_limit, Some(DEFAULT_GAS_LIMIT.to_string())); + } + #[test] fn test_map_swap_result_evm_to_evm() { let request = RelayRequest::mock( @@ -90,4 +164,29 @@ mod tests { assert_eq!(metadata.to_asset, AssetId::from_chain(Chain::Solana)); assert_eq!(metadata.to_value, "74432990"); } + + #[test] + fn test_get_step_data_by_id() { + let steps = vec![Step::mock_empty("approve", "transaction"), Step::mock_transaction("swap", "0xrouter", "0", "0xdata")]; + let data = get_step_data(&steps).unwrap(); + assert_eq!(data.to.as_deref(), Some("0xrouter")); + } + + #[test] + fn test_get_step_data_fallback_transaction_kind() { + let steps = vec![Step::mock_empty("approve", "transaction"), Step::mock_transaction("send", "0xto", "100", "0xdata")]; + let data = get_step_data(&steps).unwrap(); + assert_eq!(data.to.as_deref(), Some("0xto")); + } + + #[test] + fn test_get_step_data_empty_steps() { + assert!(get_step_data(&[]).is_err()); + } + + #[test] + fn test_get_step_data_no_usable_steps() { + let steps = vec![Step::mock_empty("approve", "transaction")]; + assert!(get_step_data(&steps).is_err()); + } } diff --git a/crates/swapper/src/relay/mod.rs b/crates/swapper/src/relay/mod.rs index 3b635ab79..01c9b7694 100644 --- a/crates/swapper/src/relay/mod.rs +++ b/crates/swapper/src/relay/mod.rs @@ -1,8 +1,40 @@ mod asset; mod chain; +mod client; mod mapper; -pub(crate) mod model; +mod model; +mod provider; #[cfg(test)] mod testkit; -pub use mapper::map_swap_result; +use std::sync::Arc; + +use crate::alien::RpcProvider; +use gem_client::Client; + +use super::{ProviderType, SwapperProvider}; + +const DEFAULT_GAS_LIMIT: u64 = 750_000; + +#[derive(Debug)] +pub struct Relay +where + C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, +{ + pub provider: ProviderType, + pub rpc_provider: Arc, + pub(crate) client: client::RelayClient, +} + +impl Relay +where + C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, +{ + pub fn with_client(client: client::RelayClient, rpc_provider: Arc) -> Self { + Self { + provider: ProviderType::new(SwapperProvider::Relay), + rpc_provider, + client, + } + } +} diff --git a/crates/swapper/src/relay/model.rs b/crates/swapper/src/relay/model.rs index 030c88b98..d7390e35e 100644 --- a/crates/swapper/src/relay/model.rs +++ b/crates/swapper/src/relay/model.rs @@ -1,6 +1,125 @@ use gem_evm::address::ethereum_address_checksum; -use primitives::swap::SwapStatus; -use serde::Deserialize; +use primitives::swap::{SwapMode, SwapStatus}; +use serde::{Deserialize, Serialize}; +use serde_serializers::deserialize_string_from_value; + +pub fn relay_trade_type(mode: &SwapMode) -> &'static str { + match mode { + SwapMode::ExactIn => "EXACT_INPUT", + SwapMode::ExactOut => "EXACT_OUTPUT", + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RelayQuoteRequest { + pub user: String, + pub origin_chain_id: u64, + pub destination_chain_id: u64, + pub origin_currency: String, + pub destination_currency: String, + pub amount: String, + pub recipient: String, + pub trade_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub refund_to: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub referrer: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub app_fees: Vec, + pub max_route_length: u32, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RelayAppFee { + pub recipient: String, + pub fee: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RelayQuoteResponse { + pub steps: Vec, + pub details: QuoteDetails, + pub fees: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RelayFees { + pub gas: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RelayFeeAmount { + pub amount: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Step { + pub id: String, + pub kind: String, + pub items: Option>, +} + +impl Step { + pub fn step_data(&self) -> Option<&StepData> { + self.items.as_ref()?.first()?.data.as_ref() + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StepItem { + pub data: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StepData { + pub to: Option, + pub data: Option, + #[serde(default, deserialize_with = "deserialize_string_from_value")] + pub value: String, + pub psbt: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct QuoteDetails { + pub currency_out: CurrencyAmount, + pub time_estimate: Option, + pub swap_impact: Option, +} + +impl QuoteDetails { + pub fn time_estimate_u32(&self) -> Option { + let value = self.time_estimate?; + if !value.is_finite() || value < 0.0 || value > u32::MAX as f64 { + return None; + } + Some(value.ceil() as u32) + } + + pub fn slippage_bps(&self) -> Option { + let percent: f64 = self.swap_impact.as_ref()?.percent.as_ref()?.parse().ok()?; + Some((percent.abs() * 100.0) as u32) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SwapImpact { + pub percent: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CurrencyAmount { + pub amount: String, +} #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "lowercase")] diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs new file mode 100644 index 000000000..10923f3f0 --- /dev/null +++ b/crates/swapper/src/relay/provider.rs @@ -0,0 +1,191 @@ +use std::sync::Arc; + +use alloy_primitives::U256; +use async_trait::async_trait; +use gem_client::Client; +use primitives::{AssetId, Chain, ChainType, swap::ApprovalData}; + +use super::{ + Relay, + asset::{SUPPORTED_CHAINS, asset_to_currency}, + chain::RelayChain, + client::RelayClient, + mapper, + model::{RelayAppFee, RelayQuoteRequest, RelayQuoteResponse, relay_trade_type}, +}; +use crate::{ + FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, RpcClient, RpcProvider, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperQuoteData, + approval::check_approval_erc20, config::get_swap_api_url, fees::resolve_max_quote_amount, referrer::DEFAULT_REFERRER, +}; + +fn resolve_app_fees(request: &QuoteRequest) -> Vec { + let Some(fee) = request.options.fee.as_ref().map(|f| &f.evm) else { + return vec![]; + }; + vec![RelayAppFee { + recipient: fee.address.clone(), + fee: fee.bps.to_string(), + }] +} + +impl Relay { + pub fn new(rpc_provider: Arc) -> Self { + let url = get_swap_api_url("relay"); + let client = RelayClient::new(RpcClient::new(url, rpc_provider.clone())); + Self::with_client(client, rpc_provider) + } +} + +#[async_trait] +impl Swapper for Relay +where + C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, +{ + fn provider(&self) -> &ProviderType { + &self.provider + } + + fn supported_assets(&self) -> Vec { + SUPPORTED_CHAINS.clone() + } + + async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + let from_chain = RelayChain::from_chain(&request.from_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; + let to_chain = RelayChain::from_chain(&request.to_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; + + let from_asset_id = request.from_asset.asset_id(); + let to_asset_id = request.to_asset.asset_id(); + + let origin_currency = asset_to_currency(&from_asset_id, &from_chain)?; + let destination_currency = asset_to_currency(&to_asset_id, &to_chain)?; + let app_fees = resolve_app_fees(request); + let amount = resolve_max_quote_amount(request)?; + + let relay_request = RelayQuoteRequest { + user: request.wallet_address.clone(), + origin_chain_id: from_chain.chain_id(), + destination_chain_id: to_chain.chain_id(), + origin_currency, + destination_currency, + amount: amount.clone(), + recipient: request.destination_address.clone(), + trade_type: relay_trade_type(&request.mode).to_string(), + referrer: if app_fees.is_empty() { None } else { Some(DEFAULT_REFERRER.to_string()) }, + app_fees, + refund_to: Some(request.wallet_address.clone()), + max_route_length: 6, + }; + + let quote_response = self.client.get_quote(relay_request).await?; + + let to_value = quote_response.details.currency_out.amount.clone(); + let eta_in_seconds = quote_response.details.time_estimate_u32(); + + let quote = Quote { + from_value: amount, + to_value, + data: ProviderData { + provider: self.provider().clone(), + routes: vec![Route { + input: from_asset_id, + output: to_asset_id, + route_data: serde_json::to_string("e_response).unwrap_or_default(), + gas_limit: None, + }], + slippage_bps: quote_response.details.slippage_bps().unwrap_or(request.options.slippage.bps), + }, + request: request.clone(), + eta_in_seconds, + }; + + Ok(quote) + } + + async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + let route = quote.data.routes.first().ok_or(SwapperError::InvalidRoute)?; + let quote_response: RelayQuoteResponse = serde_json::from_str(&route.route_data).map_err(|_| SwapperError::InvalidRoute)?; + + let from_chain = RelayChain::from_chain("e.request.from_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; + let from_asset_id = quote.request.from_asset.asset_id(); + let approval = self.check_evm_approval(quote, "e_response, &from_asset_id).await?; + mapper::map_quote_data(&from_chain, "e_response.steps, "e.from_value, "e_response.fees, approval) + } + + async fn get_swap_result(&self, _chain: Chain, transaction_hash: &str) -> Result { + let response = self.client.get_request(transaction_hash).await?; + let request = response.requests.first().ok_or(SwapperError::InvalidRoute)?; + Ok(mapper::map_swap_result(request)) + } + + async fn get_vault_addresses(&self, _from_timestamp: Option) -> Result, SwapperError> { + let response = self.client.get_chains().await?; + Ok(response.solver_addresses()) + } +} + +impl Relay +where + C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, +{ + async fn check_evm_approval(&self, quote: &Quote, quote_response: &RelayQuoteResponse, from_asset_id: &AssetId) -> Result, SwapperError> { + match from_asset_id.chain.chain_type() { + ChainType::Ethereum if !from_asset_id.is_native() => { + let router_address = quote_response + .steps + .iter() + .filter(|s| s.id != mapper::STEP_APPROVE) + .find_map(|s| s.items.as_ref()?.first().and_then(|item| item.data.as_ref().and_then(|d| d.to.clone()))) + .ok_or(SwapperError::InvalidRoute)?; + + let token = from_asset_id.token_id.clone().ok_or(SwapperError::NotSupportedAsset)?; + let amount: U256 = quote.from_value.parse().map_err(SwapperError::from)?; + + Ok(check_approval_erc20( + quote.request.wallet_address.clone(), + token, + router_address, + amount, + self.rpc_provider.clone(), + &from_asset_id.chain, + ) + .await? + .approval_data()) + } + _ => Ok(None), + } + } +} + +#[cfg(all(test, feature = "swap_integration_tests"))] +mod swap_integration_tests { + use super::*; + use crate::{SwapperMode, SwapperQuoteAsset, alien::reqwest_provider::NativeProvider, models::Options}; + use primitives::AssetId; + + #[tokio::test] + async fn test_relay_eth_to_base() -> Result<(), Box> { + use primitives::asset_constants::{USDC_ARB_ASSET_ID, USDC_BASE_ASSET_ID}; + + let provider = Arc::new(NativeProvider::default()); + let relay = Relay::new(provider); + + let request = QuoteRequest { + from_asset: SwapperQuoteAsset::from(AssetId::new(USDC_ARB_ASSET_ID).unwrap()), + to_asset: SwapperQuoteAsset::from(AssetId::new(USDC_BASE_ASSET_ID).unwrap()), + wallet_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".to_string(), + destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".to_string(), + value: "500000".to_string(), + mode: SwapperMode::ExactIn, + options: Options::new_with_slippage(100.into()), + }; + + let quote = relay.fetch_quote(&request).await?; + let quote_data = relay.fetch_quote_data("e, FetchQuoteData::None).await?; + + assert_eq!(quote.from_value, request.value); + assert!(!quote.to_value.is_empty()); + assert!(!quote_data.data.is_empty()); + + Ok(()) + } +} diff --git a/crates/swapper/src/relay/testkit.rs b/crates/swapper/src/relay/testkit.rs index 4ffccc950..c359c1274 100644 --- a/crates/swapper/src/relay/testkit.rs +++ b/crates/swapper/src/relay/testkit.rs @@ -1,4 +1,4 @@ -use super::model::{RelayCurrency, RelayCurrencyDetail, RelayRequest, RelayRequestData, RelayRequestMetadata, RelayStatus}; +use super::model::{RelayCurrency, RelayCurrencyDetail, RelayRequest, RelayRequestData, RelayRequestMetadata, RelayStatus, Step, StepData, StepItem}; impl RelayRequest { pub fn mock(status: RelayStatus, metadata: Option) -> Self { @@ -20,3 +20,28 @@ impl RelayCurrencyDetail { } } } + +impl Step { + pub fn mock_transaction(id: &str, to: &str, value: &str, data: &str) -> Self { + Self { + id: id.to_string(), + kind: "transaction".to_string(), + items: Some(vec![StepItem { + data: Some(StepData { + to: Some(to.to_string()), + data: Some(data.to_string()), + value: value.to_string(), + psbt: None, + }), + }]), + } + } + + pub fn mock_empty(id: &str, kind: &str) -> Self { + Self { + id: id.to_string(), + kind: kind.to_string(), + items: None, + } + } +} diff --git a/crates/swapper/src/swapper.rs b/crates/swapper/src/swapper.rs index a7725c86a..518538fe3 100644 --- a/crates/swapper/src/swapper.rs +++ b/crates/swapper/src/swapper.rs @@ -1,7 +1,7 @@ use crate::{ AssetList, FetchQuoteData, Permit2ApprovalData, ProviderType, Quote, QuoteRequest, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperProviderMode, - SwapperQuoteData, across, alien::RpcProvider, chainflip, config::DEFAULT_STABLE_SWAP_REFERRAL_BPS, hyperliquid, jupiter, near_intents, proxy::provider_factory, thorchain, - uniswap, + SwapperQuoteData, across, alien::RpcProvider, chainflip, config::DEFAULT_STABLE_SWAP_REFERRAL_BPS, hyperliquid, jupiter, near_intents, proxy::provider_factory, relay, + thorchain, uniswap, }; use num_traits::ToPrimitive; use primitives::{AssetId, Chain, EVMChain}; @@ -127,7 +127,7 @@ impl GemSwapper { Box::new(near_intents::NearIntents::new(rpc_provider.clone())), Box::new(chainflip::ChainflipProvider::new(rpc_provider.clone())), Box::new(provider_factory::new_cetus_aggregator(rpc_provider.clone())), - Box::new(provider_factory::new_relay(rpc_provider.clone())), + Box::new(relay::Relay::new(rpc_provider.clone())), Box::new(provider_factory::new_orca(rpc_provider.clone())), uniswap::default::boxed_aerodrome(rpc_provider.clone()), ]; From 4cd00519274b860ed9c523a81973b4736383c646 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:05:22 +0900 Subject: [PATCH 02/20] remove is_evm() check inside asset_to_currency --- crates/swapper/src/relay/asset.rs | 10 +++------- crates/swapper/src/relay/provider.rs | 6 +++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/swapper/src/relay/asset.rs b/crates/swapper/src/relay/asset.rs index ed1c7e3ef..3eff94f22 100644 --- a/crates/swapper/src/relay/asset.rs +++ b/crates/swapper/src/relay/asset.rs @@ -10,7 +10,6 @@ use primitives::{ }, }; -use super::chain::RelayChain; use crate::{SwapperChainAsset, SwapperError, asset::*}; fn is_native_currency(chain: Chain, currency: &str) -> bool { @@ -87,10 +86,7 @@ pub static SUPPORTED_CHAINS: LazyLock> = LazyLock::new(|| ] }); -pub fn asset_to_currency(asset_id: &AssetId, relay_chain: &RelayChain) -> Result { - if !relay_chain.is_evm() { - return Err(SwapperError::NotSupportedChain); - } +pub fn asset_to_currency(asset_id: &AssetId) -> Result { if asset_id.is_native() { Ok(EVM_ZERO_ADDRESS.to_string()) } else { @@ -105,14 +101,14 @@ mod tests { #[test] fn test_evm_native_asset() { - let result = asset_to_currency(&AssetId::from_chain(Chain::Ethereum), &RelayChain::from_chain(&Chain::Ethereum).unwrap()).unwrap(); + let result = asset_to_currency(&AssetId::from_chain(Chain::Ethereum)).unwrap(); assert_eq!(result, EVM_ZERO_ADDRESS); } #[test] fn test_evm_token_asset() { let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - let result = asset_to_currency(&AssetId::from_token(Chain::Ethereum, token_address), &RelayChain::from_chain(&Chain::Ethereum).unwrap()).unwrap(); + let result = asset_to_currency(&AssetId::from_token(Chain::Ethereum, token_address)).unwrap(); assert_eq!(result, token_address); } } diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs index 10923f3f0..a45618129 100644 --- a/crates/swapper/src/relay/provider.rs +++ b/crates/swapper/src/relay/provider.rs @@ -56,8 +56,8 @@ where let from_asset_id = request.from_asset.asset_id(); let to_asset_id = request.to_asset.asset_id(); - let origin_currency = asset_to_currency(&from_asset_id, &from_chain)?; - let destination_currency = asset_to_currency(&to_asset_id, &to_chain)?; + let origin_currency = asset_to_currency(&from_asset_id)?; + let destination_currency = asset_to_currency(&to_asset_id)?; let app_fees = resolve_app_fees(request); let amount = resolve_max_quote_amount(request)?; @@ -89,7 +89,7 @@ where routes: vec![Route { input: from_asset_id, output: to_asset_id, - route_data: serde_json::to_string("e_response).unwrap_or_default(), + route_data: serde_json::to_string("e_response).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?, gas_limit: None, }], slippage_bps: quote_response.details.slippage_bps().unwrap_or(request.options.slippage.bps), From f618739a63f3578def18b943f3e5f6c06d663d48 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:13:29 +0900 Subject: [PATCH 03/20] Make StepData an enum, simplify quote mapping --- crates/swapper/src/relay/chain.rs | 7 ----- crates/swapper/src/relay/mapper.rs | 36 ++++++++---------------- crates/swapper/src/relay/model.rs | 19 +++++++++++-- crates/swapper/src/relay/provider.rs | 41 ++++++++++++++++++++++++++-- crates/swapper/src/relay/testkit.rs | 9 +++--- 5 files changed, 69 insertions(+), 43 deletions(-) diff --git a/crates/swapper/src/relay/chain.rs b/crates/swapper/src/relay/chain.rs index a6c32c8cf..525c0aa09 100644 --- a/crates/swapper/src/relay/chain.rs +++ b/crates/swapper/src/relay/chain.rs @@ -44,13 +44,6 @@ impl RelayChain { } } } - - pub fn is_evm(&self) -> bool { - match self { - Self::Evm(_) => true, - Self::Bitcoin | Self::Solana => false, - } - } } #[cfg(test)] diff --git a/crates/swapper/src/relay/mapper.rs b/crates/swapper/src/relay/mapper.rs index 140b25047..dec704338 100644 --- a/crates/swapper/src/relay/mapper.rs +++ b/crates/swapper/src/relay/mapper.rs @@ -4,7 +4,7 @@ use super::{ DEFAULT_GAS_LIMIT, asset::map_currency_to_asset_id, chain::RelayChain, - model::{RelayFees, RelayRequest, Step, StepData}, + model::{RelayRequest, Step, StepData}, }; use crate::{SwapResult, SwapperError, SwapperProvider, SwapperQuoteData}; @@ -22,28 +22,16 @@ pub fn get_step_data(steps: &[Step]) -> Result<&StepData, SwapperError> { .ok_or(SwapperError::InvalidRoute) } -pub fn gas_fee_amount(fees: &Option) -> Option { - fees.as_ref()?.gas.as_ref()?.amount.clone() -} - -pub fn map_quote_data(chain: &RelayChain, steps: &[Step], value: &str, fees: &Option, approval: Option) -> Result { +pub fn map_quote_data(steps: &[Step], approval: Option) -> Result { let step_data = get_step_data(steps)?; - let (to, tx_value, data, gas_limit) = match chain { - RelayChain::Bitcoin => { - let psbt = step_data.psbt.as_ref().ok_or(SwapperError::InvalidRoute)?; - (String::new(), value.to_string(), psbt.clone(), gas_fee_amount(fees)) - } - _ if chain.is_evm() => { - let to = step_data.to.clone().unwrap_or_default(); - let data = step_data.data.clone().unwrap_or_default(); + match step_data { + StepData::Evm(evm) => { let gas_limit = approval.as_ref().map(|_| DEFAULT_GAS_LIMIT.to_string()); - (to, step_data.value.clone(), data, gas_limit) + let data = evm.data.clone().unwrap_or_default(); + Ok(SwapperQuoteData::new_contract(evm.to.clone(), evm.value.clone(), data, approval, gas_limit)) } - _ => return Err(SwapperError::NotSupportedChain), - }; - - Ok(SwapperQuoteData::new_contract(to, tx_value, data, approval, gas_limit)) + } } pub fn map_swap_result(request: &RelayRequest) -> SwapResult { @@ -76,9 +64,8 @@ mod tests { #[test] fn test_map_evm_quote_data() { let steps = vec![Step::mock_transaction("swap", "0xrouter", "1000000000000000000", "0xabcdef")]; - let chain = RelayChain::from_chain(&Chain::Ethereum).unwrap(); - let result = map_quote_data(&chain, &steps, "1000000000000000000", &None, None).unwrap(); + let result = map_quote_data(&steps, None).unwrap(); assert_eq!(result.to, "0xrouter"); assert_eq!(result.value, "1000000000000000000"); @@ -90,14 +77,13 @@ mod tests { #[test] fn test_map_evm_quote_data_with_approval() { let steps = vec![Step::mock_transaction("swap", "0xrouter", "0", "0xabcdef")]; - let chain = RelayChain::from_chain(&Chain::Ethereum).unwrap(); let approval = ApprovalData { token: "0xtoken".to_string(), spender: "0xrouter".to_string(), value: "1000".to_string(), }; - let result = map_quote_data(&chain, &steps, "1000000000000000000", &None, Some(approval.clone())).unwrap(); + let result = map_quote_data(&steps, Some(approval.clone())).unwrap(); assert_eq!(result.to, "0xrouter"); assert_eq!(result.approval, Some(approval)); @@ -169,14 +155,14 @@ mod tests { fn test_get_step_data_by_id() { let steps = vec![Step::mock_empty("approve", "transaction"), Step::mock_transaction("swap", "0xrouter", "0", "0xdata")]; let data = get_step_data(&steps).unwrap(); - assert_eq!(data.to.as_deref(), Some("0xrouter")); + assert_eq!(data.get_to().as_deref(), Some("0xrouter")); } #[test] fn test_get_step_data_fallback_transaction_kind() { let steps = vec![Step::mock_empty("approve", "transaction"), Step::mock_transaction("send", "0xto", "100", "0xdata")]; let data = get_step_data(&steps).unwrap(); - assert_eq!(data.to.as_deref(), Some("0xto")); + assert_eq!(data.get_to().as_deref(), Some("0xto")); } #[test] diff --git a/crates/swapper/src/relay/model.rs b/crates/swapper/src/relay/model.rs index d7390e35e..c24b063f3 100644 --- a/crates/swapper/src/relay/model.rs +++ b/crates/swapper/src/relay/model.rs @@ -76,14 +76,27 @@ pub struct StepItem { pub data: Option, } +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum StepData { + Evm(EvmStepData), +} + +impl StepData { + pub fn get_to(&self) -> Option { + match self { + Self::Evm(evm) => Some(evm.to.clone()), + } + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct StepData { - pub to: Option, +pub struct EvmStepData { + pub to: String, pub data: Option, #[serde(default, deserialize_with = "deserialize_string_from_value")] pub value: String, - pub psbt: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs index a45618129..9c545b310 100644 --- a/crates/swapper/src/relay/provider.rs +++ b/crates/swapper/src/relay/provider.rs @@ -105,10 +105,9 @@ where let route = quote.data.routes.first().ok_or(SwapperError::InvalidRoute)?; let quote_response: RelayQuoteResponse = serde_json::from_str(&route.route_data).map_err(|_| SwapperError::InvalidRoute)?; - let from_chain = RelayChain::from_chain("e.request.from_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; let from_asset_id = quote.request.from_asset.asset_id(); let approval = self.check_evm_approval(quote, "e_response, &from_asset_id).await?; - mapper::map_quote_data(&from_chain, "e_response.steps, "e.from_value, "e_response.fees, approval) + mapper::map_quote_data("e_response.steps, approval) } async fn get_swap_result(&self, _chain: Chain, transaction_hash: &str) -> Result { @@ -134,7 +133,7 @@ where .steps .iter() .filter(|s| s.id != mapper::STEP_APPROVE) - .find_map(|s| s.items.as_ref()?.first().and_then(|item| item.data.as_ref().and_then(|d| d.to.clone()))) + .find_map(|s| s.items.as_ref()?.first().and_then(|item| item.data.as_ref().and_then(|d| d.get_to()))) .ok_or(SwapperError::InvalidRoute)?; let token = from_asset_id.token_id.clone().ok_or(SwapperError::NotSupportedAsset)?; @@ -182,9 +181,45 @@ mod swap_integration_tests { let quote = relay.fetch_quote(&request).await?; let quote_data = relay.fetch_quote_data("e, FetchQuoteData::None).await?; + println!("quote: from_value={}, to_value={}", quote.from_value, quote.to_value); + println!("quote_data: to={}, value={}, data_len={}", quote_data.to, quote_data.value, quote_data.data.len()); + + assert_eq!(quote.from_value, request.value); + assert!(!quote.to_value.is_empty()); + assert!(!quote_data.data.is_empty()); + + Ok(()) + } + + #[tokio::test] + async fn test_relay_usdt_eth_to_base() -> Result<(), Box> { + use primitives::asset_constants::USDT_ETH_ASSET_ID; + + let provider = Arc::new(NativeProvider::default()); + let relay = Relay::new(provider); + + let request = QuoteRequest { + from_asset: SwapperQuoteAsset::from(AssetId::new(USDT_ETH_ASSET_ID).unwrap()), + to_asset: SwapperQuoteAsset::from(AssetId::from_chain(Chain::Base)), + wallet_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".to_string(), + destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".to_string(), + value: "5000000".to_string(), + mode: SwapperMode::ExactIn, + options: Options::new_with_slippage(100.into()), + }; + + let quote = relay.fetch_quote(&request).await?; + let quote_data = relay.fetch_quote_data("e, FetchQuoteData::None).await?; + + println!("quote: from_value={}, to_value={}", quote.from_value, quote.to_value); + println!("quote_data: to={}, value={}, data_len={}", quote_data.to, quote_data.value, quote_data.data.len()); + println!("approval: {:?}", quote_data.approval); + assert_eq!(quote.from_value, request.value); assert!(!quote.to_value.is_empty()); assert!(!quote_data.data.is_empty()); + assert!(!quote_data.to.is_empty()); + assert!(quote_data.approval.is_some()); Ok(()) } diff --git a/crates/swapper/src/relay/testkit.rs b/crates/swapper/src/relay/testkit.rs index c359c1274..6b86b8a89 100644 --- a/crates/swapper/src/relay/testkit.rs +++ b/crates/swapper/src/relay/testkit.rs @@ -1,4 +1,4 @@ -use super::model::{RelayCurrency, RelayCurrencyDetail, RelayRequest, RelayRequestData, RelayRequestMetadata, RelayStatus, Step, StepData, StepItem}; +use super::model::{EvmStepData, RelayCurrency, RelayCurrencyDetail, RelayRequest, RelayRequestData, RelayRequestMetadata, RelayStatus, Step, StepData, StepItem}; impl RelayRequest { pub fn mock(status: RelayStatus, metadata: Option) -> Self { @@ -27,12 +27,11 @@ impl Step { id: id.to_string(), kind: "transaction".to_string(), items: Some(vec![StepItem { - data: Some(StepData { - to: Some(to.to_string()), + data: Some(StepData::Evm(EvmStepData { + to: to.to_string(), data: Some(data.to_string()), value: value.to_string(), - psbt: None, - }), + })), }]), } } From 6c918441dcc6376c019d1312b2367df06161b27a Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:28:49 +0900 Subject: [PATCH 04/20] fix merge --- crates/swapper/src/proxy/provider.rs | 12 +----------- crates/swapper/src/relay/provider.rs | 10 +++++++--- crates/swapper/src/swapper.rs | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index 6ba34a01e..6ccfe48bc 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -273,17 +273,7 @@ where async fn get_vault_addresses(&self, _from_timestamp: Option) -> Result { match self.provider.id { - SwapperProvider::Relay => { - let base_url = get_swap_api_url("relay"); - let client = RpcClient::new(base_url, self.rpc_provider.clone()); - let chains: relay::model::RelayChainsResponse = ClientExt::get(&client, "/chains").await.map_err(SwapperError::from)?; - let addresses = chains.solver_addresses(); - Ok(VaultAddresses { - deposit: addresses.clone(), - send: addresses, - }) - } - SwapperProvider::Mayan => { +SwapperProvider::Mayan => { let base_url = get_swap_api_url("mayan/price"); let client = MayanPrice::new(base_url, self.rpc_provider.clone()); let api_addresses = client.get_chains().await.map(MayanChain::unique_addresses).unwrap_or_default(); diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs index 9c545b310..b5de091ee 100644 --- a/crates/swapper/src/relay/provider.rs +++ b/crates/swapper/src/relay/provider.rs @@ -15,7 +15,7 @@ use super::{ }; use crate::{ FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, RpcClient, RpcProvider, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperQuoteData, - approval::check_approval_erc20, config::get_swap_api_url, fees::resolve_max_quote_amount, referrer::DEFAULT_REFERRER, + approval::check_approval_erc20, config::get_swap_api_url, cross_chain::VaultAddresses, fees::resolve_max_quote_amount, referrer::DEFAULT_REFERRER, }; fn resolve_app_fees(request: &QuoteRequest) -> Vec { @@ -116,9 +116,13 @@ where Ok(mapper::map_swap_result(request)) } - async fn get_vault_addresses(&self, _from_timestamp: Option) -> Result, SwapperError> { + async fn get_vault_addresses(&self, _from_timestamp: Option) -> Result { let response = self.client.get_chains().await?; - Ok(response.solver_addresses()) + let addresses = response.solver_addresses(); + Ok(VaultAddresses { + deposit: addresses.clone(), + send: addresses, + }) } } diff --git a/crates/swapper/src/swapper.rs b/crates/swapper/src/swapper.rs index 01f1234d6..19e18c148 100644 --- a/crates/swapper/src/swapper.rs +++ b/crates/swapper/src/swapper.rs @@ -1,7 +1,7 @@ use crate::{ AssetList, FetchQuoteData, Permit2ApprovalData, ProviderType, Quote, QuoteRequest, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperProviderMode, SwapperQuoteData, across, alien::RpcProvider, chainflip, config::DEFAULT_STABLE_SWAP_REFERRAL_BPS, cross_chain::VaultAddresses, hyperliquid, jupiter, near_intents, - proxy::provider_factory, thorchain, uniswap, + proxy::provider_factory, relay, thorchain, uniswap, }; use num_traits::ToPrimitive; use primitives::{AssetId, Chain, EVMChain}; From 6c3cd854171de73ebb61d5eaed5751acabc14a44 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:08:54 +0900 Subject: [PATCH 05/20] Fix format --- crates/swapper/src/proxy/provider.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index 6ccfe48bc..e4bec20f4 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -273,7 +273,7 @@ where async fn get_vault_addresses(&self, _from_timestamp: Option) -> Result { match self.provider.id { -SwapperProvider::Mayan => { + SwapperProvider::Mayan => { let base_url = get_swap_api_url("mayan/price"); let client = MayanPrice::new(base_url, self.rpc_provider.clone()); let api_addresses = client.get_chains().await.map(MayanChain::unique_addresses).unwrap_or_default(); From 01281f43adebc4679a2239f9b19062c97b3842dd Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:26:53 +0900 Subject: [PATCH 06/20] code cleanup --- crates/swapper/src/relay/asset.rs | 2 ++ crates/swapper/src/relay/mod.rs | 6 +++--- crates/swapper/src/relay/provider.rs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/swapper/src/relay/asset.rs b/crates/swapper/src/relay/asset.rs index 3eff94f22..d9489c008 100644 --- a/crates/swapper/src/relay/asset.rs +++ b/crates/swapper/src/relay/asset.rs @@ -79,6 +79,8 @@ pub static SUPPORTED_CHAINS: LazyLock> = LazyLock::new(|| vec![AssetId::from_chain(Chain::Hyperliquid), USDC_HYPEREVM_ASSET_ID.into(), USDT_HYPEREVM_ASSET_ID.into()], ), SwapperChainAsset::Assets(Chain::Berachain, vec![]), + SwapperChainAsset::Assets(Chain::Manta, vec![]), + SwapperChainAsset::Assets(Chain::Sonic, vec![]), SwapperChainAsset::Assets(Chain::Abstract, vec![]), SwapperChainAsset::Assets(Chain::Mantle, vec![]), SwapperChainAsset::Assets(Chain::Celo, vec![]), diff --git a/crates/swapper/src/relay/mod.rs b/crates/swapper/src/relay/mod.rs index 01c9b7694..1c5e7436c 100644 --- a/crates/swapper/src/relay/mod.rs +++ b/crates/swapper/src/relay/mod.rs @@ -21,9 +21,9 @@ pub struct Relay where C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, { - pub provider: ProviderType, - pub rpc_provider: Arc, - pub(crate) client: client::RelayClient, + provider: ProviderType, + rpc_provider: Arc, + client: client::RelayClient, } impl Relay diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs index b5de091ee..019ebab2d 100644 --- a/crates/swapper/src/relay/provider.rs +++ b/crates/swapper/src/relay/provider.rs @@ -137,7 +137,7 @@ where .steps .iter() .filter(|s| s.id != mapper::STEP_APPROVE) - .find_map(|s| s.items.as_ref()?.first().and_then(|item| item.data.as_ref().and_then(|d| d.get_to()))) + .find_map(|s| s.step_data()?.get_to()) .ok_or(SwapperError::InvalidRoute)?; let token = from_asset_id.token_id.clone().ok_or(SwapperError::NotSupportedAsset)?; From f12f40c8043fc4e352bf2320b1c8e337435dcf3c Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:15:37 +0900 Subject: [PATCH 07/20] remove extra chain asset from vec --- crates/swapper/src/relay/asset.rs | 44 +++++++------------------------ 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/crates/swapper/src/relay/asset.rs b/crates/swapper/src/relay/asset.rs index d9489c008..8050bb028 100644 --- a/crates/swapper/src/relay/asset.rs +++ b/crates/swapper/src/relay/asset.rs @@ -37,47 +37,23 @@ pub static SUPPORTED_CHAINS: LazyLock> = LazyLock::new(|| vec![ SwapperChainAsset::Assets( Chain::Ethereum, - vec![ - AssetId::from_chain(Chain::Ethereum), - AssetId::from_token(Chain::Ethereum, ETHEREUM_USDC_TOKEN_ID), - AssetId::from_token(Chain::Ethereum, ETHEREUM_USDT_TOKEN_ID), - ], + vec![AssetId::from_token(Chain::Ethereum, ETHEREUM_USDC_TOKEN_ID), AssetId::from_token(Chain::Ethereum, ETHEREUM_USDT_TOKEN_ID)], ), SwapperChainAsset::Assets( Chain::SmartChain, - vec![ - AssetId::from_chain(Chain::SmartChain), - AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDC_TOKEN_ID), - AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDT_TOKEN_ID), - ], - ), - SwapperChainAsset::Assets(Chain::Base, vec![AssetId::from_chain(Chain::Base), AssetId::from_token(Chain::Base, BASE_USDC_TOKEN_ID)]), - SwapperChainAsset::Assets( - Chain::Arbitrum, - vec![AssetId::from_chain(Chain::Arbitrum), USDC_ARB_ASSET_ID.into(), USDT_ARB_ASSET_ID.into()], - ), - SwapperChainAsset::Assets( - Chain::Optimism, - vec![AssetId::from_chain(Chain::Optimism), USDC_OP_ASSET_ID.into(), USDT_OP_ASSET_ID.into()], - ), - SwapperChainAsset::Assets( - Chain::Polygon, - vec![AssetId::from_chain(Chain::Polygon), USDC_POLYGON_ASSET_ID.into(), USDT_POLYGON_ASSET_ID.into()], + vec![AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDC_TOKEN_ID), AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDT_TOKEN_ID)], ), + SwapperChainAsset::Assets(Chain::Base, vec![AssetId::from_token(Chain::Base, BASE_USDC_TOKEN_ID)]), + SwapperChainAsset::Assets(Chain::Arbitrum, vec![USDC_ARB_ASSET_ID.into(), USDT_ARB_ASSET_ID.into()]), + SwapperChainAsset::Assets(Chain::Optimism, vec![USDC_OP_ASSET_ID.into(), USDT_OP_ASSET_ID.into()]), + SwapperChainAsset::Assets(Chain::Polygon, vec![USDC_POLYGON_ASSET_ID.into(), USDT_POLYGON_ASSET_ID.into()]), SwapperChainAsset::Assets( Chain::AvalancheC, - vec![ - AssetId::from_chain(Chain::AvalancheC), - AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDC_TOKEN_ID), - AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDT_TOKEN_ID), - ], - ), - SwapperChainAsset::Assets(Chain::Linea, vec![AssetId::from_chain(Chain::Linea), USDT_LINEA_ASSET_ID.into()]), - SwapperChainAsset::Assets(Chain::ZkSync, vec![AssetId::from_chain(Chain::ZkSync), USDT_ZKSYNC_ASSET_ID.into()]), - SwapperChainAsset::Assets( - Chain::Hyperliquid, - vec![AssetId::from_chain(Chain::Hyperliquid), USDC_HYPEREVM_ASSET_ID.into(), USDT_HYPEREVM_ASSET_ID.into()], + vec![AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDC_TOKEN_ID), AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDT_TOKEN_ID)], ), + SwapperChainAsset::Assets(Chain::Linea, vec![USDT_LINEA_ASSET_ID.into()]), + SwapperChainAsset::Assets(Chain::ZkSync, vec![USDT_ZKSYNC_ASSET_ID.into()]), + SwapperChainAsset::Assets(Chain::Hyperliquid, vec![USDC_HYPEREVM_ASSET_ID.into(), USDT_HYPEREVM_ASSET_ID.into()]), SwapperChainAsset::Assets(Chain::Berachain, vec![]), SwapperChainAsset::Assets(Chain::Manta, vec![]), SwapperChainAsset::Assets(Chain::Sonic, vec![]), From ff18e134c5dbe2fb08c7c43401fadb306639edc6 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:27:50 +0900 Subject: [PATCH 08/20] lower default swap gas limit for relay --- crates/swapper/src/relay/asset.rs | 1 - crates/swapper/src/relay/mapper.rs | 11 +++-------- crates/swapper/src/relay/mod.rs | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/swapper/src/relay/asset.rs b/crates/swapper/src/relay/asset.rs index 8050bb028..dee7a0b2f 100644 --- a/crates/swapper/src/relay/asset.rs +++ b/crates/swapper/src/relay/asset.rs @@ -58,7 +58,6 @@ pub static SUPPORTED_CHAINS: LazyLock> = LazyLock::new(|| SwapperChainAsset::Assets(Chain::Manta, vec![]), SwapperChainAsset::Assets(Chain::Sonic, vec![]), SwapperChainAsset::Assets(Chain::Abstract, vec![]), - SwapperChainAsset::Assets(Chain::Mantle, vec![]), SwapperChainAsset::Assets(Chain::Celo, vec![]), SwapperChainAsset::Assets(Chain::Stable, vec![]), ] diff --git a/crates/swapper/src/relay/mapper.rs b/crates/swapper/src/relay/mapper.rs index dec704338..2aa95c64b 100644 --- a/crates/swapper/src/relay/mapper.rs +++ b/crates/swapper/src/relay/mapper.rs @@ -1,11 +1,6 @@ use primitives::{TransactionSwapMetadata, swap::ApprovalData}; -use super::{ - DEFAULT_GAS_LIMIT, - asset::map_currency_to_asset_id, - chain::RelayChain, - model::{RelayRequest, Step, StepData}, -}; +use super::{DEFAULT_SWAP_GAS_LIMIT, asset::map_currency_to_asset_id, chain::RelayChain, model::{RelayRequest, Step, StepData}}; use crate::{SwapResult, SwapperError, SwapperProvider, SwapperQuoteData}; pub const STEP_SWAP: &str = "swap"; @@ -27,7 +22,7 @@ pub fn map_quote_data(steps: &[Step], approval: Option) -> Result< match step_data { StepData::Evm(evm) => { - let gas_limit = approval.as_ref().map(|_| DEFAULT_GAS_LIMIT.to_string()); + let gas_limit = approval.as_ref().map(|_| DEFAULT_SWAP_GAS_LIMIT.to_string()); let data = evm.data.clone().unwrap_or_default(); Ok(SwapperQuoteData::new_contract(evm.to.clone(), evm.value.clone(), data, approval, gas_limit)) } @@ -87,7 +82,7 @@ mod tests { assert_eq!(result.to, "0xrouter"); assert_eq!(result.approval, Some(approval)); - assert_eq!(result.gas_limit, Some(DEFAULT_GAS_LIMIT.to_string())); + assert_eq!(result.gas_limit, Some(DEFAULT_SWAP_GAS_LIMIT.to_string())); } #[test] diff --git a/crates/swapper/src/relay/mod.rs b/crates/swapper/src/relay/mod.rs index 1c5e7436c..a9990a8a8 100644 --- a/crates/swapper/src/relay/mod.rs +++ b/crates/swapper/src/relay/mod.rs @@ -14,7 +14,7 @@ use gem_client::Client; use super::{ProviderType, SwapperProvider}; -const DEFAULT_GAS_LIMIT: u64 = 750_000; +const DEFAULT_SWAP_GAS_LIMIT: u64 = 150_000; #[derive(Debug)] pub struct Relay From 19b36f39e4c3433d5fad8a954bf80cfb1ba14eb1 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:40:23 +0900 Subject: [PATCH 09/20] code cleanup --- crates/swapper/src/relay/mod.rs | 30 +----------------------- crates/swapper/src/relay/provider.rs | 35 +++++++++++++++++++--------- 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/crates/swapper/src/relay/mod.rs b/crates/swapper/src/relay/mod.rs index a9990a8a8..6a0dbe712 100644 --- a/crates/swapper/src/relay/mod.rs +++ b/crates/swapper/src/relay/mod.rs @@ -7,34 +7,6 @@ mod provider; #[cfg(test)] mod testkit; -use std::sync::Arc; - -use crate::alien::RpcProvider; -use gem_client::Client; - -use super::{ProviderType, SwapperProvider}; - const DEFAULT_SWAP_GAS_LIMIT: u64 = 150_000; -#[derive(Debug)] -pub struct Relay -where - C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, -{ - provider: ProviderType, - rpc_provider: Arc, - client: client::RelayClient, -} - -impl Relay -where - C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, -{ - pub fn with_client(client: client::RelayClient, rpc_provider: Arc) -> Self { - Self { - provider: ProviderType::new(SwapperProvider::Relay), - rpc_provider, - client, - } - } -} +pub use provider::Relay; diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs index 019ebab2d..f3c3061d1 100644 --- a/crates/swapper/src/relay/provider.rs +++ b/crates/swapper/src/relay/provider.rs @@ -6,7 +6,6 @@ use gem_client::Client; use primitives::{AssetId, Chain, ChainType, swap::ApprovalData}; use super::{ - Relay, asset::{SUPPORTED_CHAINS, asset_to_currency}, chain::RelayChain, client::RelayClient, @@ -14,10 +13,32 @@ use super::{ model::{RelayAppFee, RelayQuoteRequest, RelayQuoteResponse, relay_trade_type}, }; use crate::{ - FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, RpcClient, RpcProvider, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperQuoteData, - approval::check_approval_erc20, config::get_swap_api_url, cross_chain::VaultAddresses, fees::resolve_max_quote_amount, referrer::DEFAULT_REFERRER, + FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, RpcClient, RpcProvider, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, + SwapperQuoteData, approval::check_approval_erc20, config::get_swap_api_url, cross_chain::VaultAddresses, fees::resolve_max_quote_amount, referrer::DEFAULT_REFERRER, }; +#[derive(Debug)] +pub struct Relay +where + C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, +{ + provider: ProviderType, + rpc_provider: Arc, + client: RelayClient, +} + +impl Relay { + pub fn new(rpc_provider: Arc) -> Self { + let url = get_swap_api_url("relay"); + let client = RelayClient::new(RpcClient::new(url, rpc_provider.clone())); + Self { + provider: ProviderType::new(SwapperProvider::Relay), + rpc_provider, + client, + } + } +} + fn resolve_app_fees(request: &QuoteRequest) -> Vec { let Some(fee) = request.options.fee.as_ref().map(|f| &f.evm) else { return vec![]; @@ -28,14 +49,6 @@ fn resolve_app_fees(request: &QuoteRequest) -> Vec { }] } -impl Relay { - pub fn new(rpc_provider: Arc) -> Self { - let url = get_swap_api_url("relay"); - let client = RelayClient::new(RpcClient::new(url, rpc_provider.clone())); - Self::with_client(client, rpc_provider) - } -} - #[async_trait] impl Swapper for Relay where From bf186cf41712d4a979c7834b9d92daea5eb14398 Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:33:54 +0000 Subject: [PATCH 10/20] Add relay mapper tests and testdata Add two unit tests to crates/swapper/src/relay/mapper.rs that exercise map_swap_result using RelayRequestsResponse fixtures for Base USDC -> Ethereum USDC and Base USDC -> Solana USDT. The tests assert SwapStatus::Completed and validate metadata fields (from_asset/from_value and to_asset/to_value). Add the corresponding JSON fixtures under crates/swapper/src/relay/testdata/ (request_base_usdc_to_eth_usdc.json and request_base_usdc_to_sol_usdt.json). --- crates/swapper/src/relay/mapper.rs | 28 ++ .../request_base_usdc_to_eth_usdc.json | 276 +++++++++++++++ .../request_base_usdc_to_sol_usdt.json | 322 ++++++++++++++++++ 3 files changed, 626 insertions(+) create mode 100644 crates/swapper/src/relay/testdata/request_base_usdc_to_eth_usdc.json create mode 100644 crates/swapper/src/relay/testdata/request_base_usdc_to_sol_usdt.json diff --git a/crates/swapper/src/relay/mapper.rs b/crates/swapper/src/relay/mapper.rs index 2aa95c64b..f52c79ca8 100644 --- a/crates/swapper/src/relay/mapper.rs +++ b/crates/swapper/src/relay/mapper.rs @@ -170,4 +170,32 @@ mod tests { let steps = vec![Step::mock_empty("approve", "transaction")]; assert!(get_step_data(&steps).is_err()); } + + #[test] + fn test_map_swap_result_base_usdc_to_eth_usdc() { + let response: RelayRequestsResponse = serde_json::from_str(include_str!("testdata/request_base_usdc_to_eth_usdc.json")).unwrap(); + let request = response.requests.first().unwrap(); + let result = map_swap_result(request); + + assert_eq!(result.status, SwapStatus::Completed); + let metadata = result.metadata.unwrap(); + assert_eq!(metadata.from_asset, AssetId::from_token(Chain::Base, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")); + assert_eq!(metadata.from_value, "11911543"); + assert_eq!(metadata.to_asset, AssetId::from_token(Chain::Ethereum, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")); + assert_eq!(metadata.to_value, "11707349"); + } + + #[test] + fn test_map_swap_result_base_usdc_to_sol_usdt() { + let response: RelayRequestsResponse = serde_json::from_str(include_str!("testdata/request_base_usdc_to_sol_usdt.json")).unwrap(); + let request = response.requests.first().unwrap(); + let result = map_swap_result(request); + + assert_eq!(result.status, SwapStatus::Completed); + let metadata = result.metadata.unwrap(); + assert_eq!(metadata.from_asset, AssetId::from_token(Chain::Base, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")); + assert_eq!(metadata.from_value, "3000000"); + assert_eq!(metadata.to_asset, AssetId::from_token(Chain::Solana, "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB")); + assert_eq!(metadata.to_value, "2960498"); + } } diff --git a/crates/swapper/src/relay/testdata/request_base_usdc_to_eth_usdc.json b/crates/swapper/src/relay/testdata/request_base_usdc_to_eth_usdc.json new file mode 100644 index 000000000..e62ff0ce2 --- /dev/null +++ b/crates/swapper/src/relay/testdata/request_base_usdc_to_eth_usdc.json @@ -0,0 +1,276 @@ +{ + "requests": [ + { + "id": "0x8f56957067e24868a69c22d858f841a9266c83c1045c16e5bb8c1da7f6061703", + "status": "success", + "user": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", + "recipient": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", + "data": { + "slippageTolerance": "200", + "failReason": "N/A", + "refundFailReason": "N/A", + "subsidizedRequest": false, + "fees": { + "gas": "96924", + "fixed": "20002", + "price": "936", + "gateway": "0" + }, + "feesUsd": { + "gas": "0.096921", + "fixed": "0.020001", + "price": "0.000935", + "gateway": "0.000000" + }, + "inTxs": [ + { + "fee": "847893217239", + "data": { + "to": "0x4cd00e387622c35bddb9b4c962c136462338bc31", + "data": "0xe8017952000000000000000000000000514bcb1f9aabb904e6106bd1052b66d2706dbbb7000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000b5c17701aad3b5e755a3fe465bd1d314a091638e2316e6f0a9caf29783d57f67c8b7b8", + "from": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", + "value": "0" + }, + "stateChanges": [ + { + "change": { + "data": { + "tokenKind": "ft", + "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" + }, + "kind": "token", + "balanceDiff": "-11911543" + }, + "address": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7" + }, + { + "change": { + "data": { + "tokenKind": "ft", + "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" + }, + "kind": "token", + "balanceDiff": "11911543" + }, + "address": "0x4cd00e387622c35bddb9b4c962c136462338bc31" + } + ], + "hash": "0xba65690811a4dd3efb444c1eacaa7a6255436b1e8c58ae56bdb110b3ccc39084", + "block": 42954356, + "type": "onchain", + "chainId": 8453, + "timestamp": 1772698059, + "status": "success" + } + ], + "currency": "usdc", + "currencyObject": {}, + "feeCurrency": "usdc", + "feeCurrencyObject": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "appFeeCurrencyObject": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "appFees": [ + { + "recipient": "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC", + "bps": "25", + "amount": "29778", + "amountUsd": "0.029777", + "amountUsdCurrent": "0.029777" + } + ], + "paidAppFees": [ + { + "recipient": "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC", + "bps": "25", + "amount": "29778", + "amountUsd": "0.029777", + "amountUsdCurrent": "0.029777" + } + ], + "metadata": { + "sender": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", + "recipient": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", + "currencyIn": { + "currency": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "amount": "11911543", + "amountFormatted": "11.911543", + "amountUsd": "11.909899", + "amountUsdCurrent": "11.910959", + "minimumAmount": "11911543" + }, + "currencyOut": { + "currency": { + "chainId": 1, + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "amount": "11707349", + "amountFormatted": "11.707349", + "amountUsd": "11.705932", + "amountUsdCurrent": "11.706775", + "minimumAmount": "11473202" + }, + "rate": "0.9828574685916006", + "route": { + "origin": { + "inputCurrency": { + "currency": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "amount": "11911543", + "amountFormatted": "11.911543", + "amountUsd": "11.909899", + "minimumAmount": "11911543" + }, + "outputCurrency": { + "currency": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "amount": "11911543", + "amountFormatted": "11.911543", + "amountUsd": "11.909899", + "minimumAmount": "11911543" + }, + "router": "relay" + }, + "destination": { + "inputCurrency": { + "currency": { + "chainId": 1, + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "amount": "11707349", + "amountFormatted": "11.707349", + "amountUsd": "11.705733", + "minimumAmount": "11473202" + }, + "outputCurrency": { + "currency": { + "chainId": 1, + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "amount": "11707349", + "amountFormatted": "11.707349", + "amountUsd": "11.705733", + "minimumAmount": "11473202" + }, + "router": "relay" + } + } + }, + "price": "11707349", + "usesExternalLiquidity": false, + "timeEstimate": 7, + "outTxs": [ + { + "fee": "5764046694132", + "data": { + "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "data": "0x23b872dd000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef000000000000000000000000514bcb1f9aabb904e6106bd1052b66d2706dbbb70000000000000000000000000000000000000000000000000000000000b2a3d501aad3b5e755a3fe465bd1d314a091638e2316e6f0a9caf29783d57f67c8b7b8", + "from": "0xada5bb90d0de0bd1b6f3938708f49295a8d1f7cb", + "value": "0" + }, + "stateChanges": [ + { + "change": { + "data": { + "tokenKind": "ft", + "tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + }, + "kind": "token", + "balanceDiff": "-11707349" + }, + "address": "0xf70da97812cb96acdf810712aa562db8dfa3dbef" + }, + { + "change": { + "data": { + "tokenKind": "ft", + "tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + }, + "kind": "token", + "balanceDiff": "11707349" + }, + "address": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7" + } + ], + "hash": "0x3fee130ba0a8a4c1b2b75f16f6052f7cc749fb4d89b9777c97c35ed514af3399", + "block": 24589972, + "type": "onchain", + "chainId": 1, + "timestamp": 1772698103, + "status": "success" + } + ] + }, + "referrer": "gemwallet", + "createdAt": "2026-03-05T08:07:38.367Z", + "updatedAt": "2026-03-05T08:10:31.371Z" + } + ] +} diff --git a/crates/swapper/src/relay/testdata/request_base_usdc_to_sol_usdt.json b/crates/swapper/src/relay/testdata/request_base_usdc_to_sol_usdt.json new file mode 100644 index 000000000..98e2d38c1 --- /dev/null +++ b/crates/swapper/src/relay/testdata/request_base_usdc_to_sol_usdt.json @@ -0,0 +1,322 @@ +{ + "requests": [ + { + "id": "0x394b33c90fa8c29e1ce0c7bd1b728d28753ecb7c272ca50f6b195734d48dfa5b", + "status": "success", + "user": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", + "recipient": "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC", + "data": { + "slippageTolerance": "274", + "failReason": "N/A", + "refundFailReason": "N/A", + "subsidizedRequest": false, + "fees": { + "gas": "3657", + "fixed": "20000", + "price": "873", + "gateway": "7174" + }, + "feesUsd": { + "gas": "0.003656", + "fixed": "0.019999", + "price": "0.000872", + "gateway": "0.007173" + }, + "inTxs": [ + { + "fee": "764163694615", + "data": { + "to": "0x4cd00e387622c35bddb9b4c962c136462338bc31", + "data": "0xe8017952000000000000000000000000514bcb1f9aabb904e6106bd1052b66d2706dbbb7000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000002dc6c046a15fef562c289f6203afe32324e57129afadff4b86f5e40dcca0b4f5d4c652", + "from": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", + "value": "0" + }, + "stateChanges": [ + { + "change": { + "data": { + "tokenKind": "ft", + "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" + }, + "kind": "token", + "balanceDiff": "-3000000" + }, + "address": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7" + }, + { + "change": { + "data": { + "tokenKind": "ft", + "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" + }, + "kind": "token", + "balanceDiff": "3000000" + }, + "address": "0x4cd00e387622c35bddb9b4c962c136462338bc31" + } + ], + "hash": "0x9d99b1a71ac0b3d4b5eb4ea59a9991e954620839692a2aa99fb24490030d351a", + "block": 42747491, + "type": "onchain", + "chainId": 8453, + "timestamp": 1772284329, + "status": "success" + } + ], + "currency": "usdt", + "currencyObject": {}, + "feeCurrency": "usdc", + "feeCurrencyObject": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "appFeeCurrencyObject": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "appFees": [ + { + "recipient": "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC", + "bps": "25", + "amount": "7500", + "amountUsd": "0.007499", + "amountUsdCurrent": "0.007499" + } + ], + "paidAppFees": [ + { + "recipient": "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC", + "bps": "25", + "amount": "7500", + "amountUsd": "0.007499", + "amountUsdCurrent": "0.007499" + } + ], + "metadata": { + "sender": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", + "recipient": "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC", + "currencyIn": { + "currency": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "amount": "3000000", + "amountFormatted": "3.0", + "amountUsd": "2.999994", + "amountUsdCurrent": "2.999853", + "minimumAmount": "3000000" + }, + "currencyOut": { + "currency": { + "chainId": 792703809, + "address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + "symbol": "USDT", + "name": "USDT", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/325/large/Tether.png?1696501661", + "verified": true + } + }, + "amount": "2960498", + "amountFormatted": "2.960498", + "amountUsd": "2.960492", + "amountUsdCurrent": "2.960353", + "minimumAmount": "2879355" + }, + "rate": "0.9868326666666666", + "route": { + "origin": { + "inputCurrency": { + "currency": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "amount": "3000000", + "amountFormatted": "3.0", + "amountUsd": "2.999994", + "minimumAmount": "3000000" + }, + "outputCurrency": { + "currency": { + "chainId": 8453, + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", + "verified": true + } + }, + "amount": "3000000", + "amountFormatted": "3.0", + "amountUsd": "2.999994", + "minimumAmount": "3000000" + }, + "router": "relay" + }, + "destination": { + "inputCurrency": { + "currency": { + "chainId": 792703809, + "address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + "symbol": "USDT", + "name": "USDT", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/325/large/Tether.png?1696501661", + "verified": true + } + }, + "amount": "2960472", + "amountFormatted": "2.960472", + "amountUsd": "2.960466", + "minimumAmount": "2879355" + }, + "outputCurrency": { + "currency": { + "chainId": 792703809, + "address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + "symbol": "USDT", + "name": "USDT", + "decimals": 6, + "metadata": { + "logoURI": "https://coin-images.coingecko.com/coins/images/325/large/Tether.png?1696501661", + "verified": true + } + }, + "amount": "2960472", + "amountFormatted": "2.960472", + "amountUsd": "2.960466", + "minimumAmount": "2879355" + }, + "router": "relay" + } + } + }, + "price": "2960498", + "usesExternalLiquidity": false, + "timeEstimate": 1, + "outTxs": [ + { + "fee": "142612", + "data": { + "signer": "F7p3dFrjRTbtRp8FRF6qHLomXbKRBzpvBLjtQcfcgmNe", + "instructions": [ + { + "data": "GAxM3u", + "keys": [], + "programId": "ComputeBudget111111111111111111111111111111" + }, + { + "data": "3HnbFZFth191", + "keys": [], + "programId": "ComputeBudget111111111111111111111111111111" + }, + { + "data": "3YZBNpy9smUb", + "keys": [ + { + "pubkey": "A2sPEp16HY5pqxrSeyb78wevY1UQtuoBAGXmG3TNmaU9", + "isSigner": false, + "isWritable": true + }, + { + "pubkey": "Bbkfm7UsDMY194BiWcT64MQ46Afbbe7zitL5taGeLRuB", + "isSigner": false, + "isWritable": true + }, + { + "pubkey": "F7p3dFrjRTbtRp8FRF6qHLomXbKRBzpvBLjtQcfcgmNe", + "isSigner": true, + "isWritable": true + } + ], + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "data": "KsyQypGSzaMRjJXpPsegPR7Qx65p45wBRviG8MiEJDn3AQPwvPUcCJYwyvw4mM7UrfFCn5s7gWM29kdY25w3dzfSmw", + "keys": [], + "programId": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" + } + ] + }, + "stateChanges": [ + { + "change": { + "data": { + "tokenKind": "ft", + "tokenAddress": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" + }, + "kind": "token", + "balanceDiff": "-2960498" + }, + "address": "F7p3dFrjRTbtRp8FRF6qHLomXbKRBzpvBLjtQcfcgmNe" + }, + { + "change": { + "data": { + "tokenKind": "ft", + "tokenAddress": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" + }, + "kind": "token", + "balanceDiff": "2960498" + }, + "address": "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC" + }, + { + "change": { + "data": { + "tokenKind": "ft", + "tokenAddress": "11111111111111111111111111111111" + }, + "kind": "token", + "balanceDiff": "-142612" + }, + "address": "F7p3dFrjRTbtRp8FRF6qHLomXbKRBzpvBLjtQcfcgmNe" + } + ], + "hash": "3VMoJY5LiVU93J9jAQ23Py77rEp1z8xLUKRu2wcGfEvifuRhF4C1rL7y9PjjrGGQ7BknMDTXfiUv92wJAKe1XoSK", + "block": 403288806, + "type": "onchain", + "chainId": 792703809, + "timestamp": 1772284328, + "status": "success" + } + ] + }, + "referrer": "gemwallet", + "createdAt": "2026-02-28T13:12:07.863Z", + "updatedAt": "2026-02-28T13:12:18.372Z" + } + ] +} From 52c530d5463d6effe898303910d5f319ae7ba7db Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:22:11 +0900 Subject: [PATCH 11/20] more code cleanup --- crates/swapper/src/fees.rs | 2 +- crates/swapper/src/near_intents/provider.rs | 10 +- crates/swapper/src/relay/asset.rs | 33 ++++- crates/swapper/src/relay/chain.rs | 34 ++--- crates/swapper/src/relay/mapper.rs | 116 ++++++--------- crates/swapper/src/relay/model.rs | 133 +++++++++++++++--- crates/swapper/src/relay/provider.rs | 26 ++-- .../testdata/request_bsc_usdt_to_sol.json | 38 ----- .../relay/testdata/request_eth_to_btc.json | 42 ------ 9 files changed, 207 insertions(+), 227 deletions(-) delete mode 100644 crates/swapper/src/relay/testdata/request_bsc_usdt_to_sol.json delete mode 100644 crates/swapper/src/relay/testdata/request_eth_to_btc.json diff --git a/crates/swapper/src/fees.rs b/crates/swapper/src/fees.rs index 2c0c7ff0e..d1fac3016 100644 --- a/crates/swapper/src/fees.rs +++ b/crates/swapper/src/fees.rs @@ -40,7 +40,7 @@ pub fn reserved_tx_fees(chain: Chain) -> Option<&'static str> { RESERVED_NATIVE_FEES.get(&chain).copied() } -pub fn resolve_max_quote_amount(request: &QuoteRequest) -> Result { +pub fn resolve_max_quote_value(request: &QuoteRequest) -> Result { if !request.options.use_max_amount || !request.from_asset.asset_id().is_native() { return Ok(request.value.clone()); } diff --git a/crates/swapper/src/near_intents/provider.rs b/crates/swapper/src/near_intents/provider.rs index 255b994ba..8f3608fc8 100644 --- a/crates/swapper/src/near_intents/provider.rs +++ b/crates/swapper/src/near_intents/provider.rs @@ -9,7 +9,7 @@ use crate::{ SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, amount_to_value, client_factory::create_client_with_chain, cross_chain::VaultAddresses, - fees::resolve_max_quote_amount, + fees::resolve_max_quote_value, near_intents::client::{base_url, explorer_url}, referrer::DEFAULT_REFERRER, }; @@ -288,7 +288,7 @@ where SwapperMode::ExactOut => return Err(SwapperError::NotSupportedAsset), }; - let amount = resolve_max_quote_amount(request)?; + let amount = resolve_max_quote_value(request)?; let quote_request = self.build_quote_request(request, mode, amount.clone(), true)?; let response = Self::extract_quote(self.client.fetch_quote("e_request).await?, request.from_asset.decimals)?; let amount_out = Self::parse_amount(&response.quote.amount_out, "amountOut")?; @@ -425,7 +425,7 @@ mod tests { let amount = (reserve + U256::from(500u64)).to_string(); let request = build_quote_request(&amount, true, Chain::Ethereum); - let result = resolve_max_quote_amount(&request).expect("expected amount to resolve"); + let result = resolve_max_quote_value(&request).expect("expected amount to resolve"); assert_eq!(result, (U256::from_str(&amount).unwrap() - reserve).to_string()); } @@ -434,7 +434,7 @@ mod tests { fn resolve_quote_amount_without_use_max_keeps_amount() { let amount = "123456"; let request = build_quote_request(amount, false, Chain::Ethereum); - let result = resolve_max_quote_amount(&request).expect("expected amount to resolve"); + let result = resolve_max_quote_value(&request).expect("expected amount to resolve"); assert_eq!(result, amount); } @@ -444,7 +444,7 @@ mod tests { let reserve = U256::from_str(reserved_tx_fees(Chain::Ethereum).unwrap()).unwrap(); let request = build_quote_request(&reserve.to_string(), true, Chain::Ethereum); - let err = resolve_max_quote_amount(&request).expect_err("expected error"); + let err = resolve_max_quote_value(&request).expect_err("expected error"); assert!(matches!(err, SwapperError::InputAmountError { .. })); } diff --git a/crates/swapper/src/relay/asset.rs b/crates/swapper/src/relay/asset.rs index dee7a0b2f..5bbf37d39 100644 --- a/crates/swapper/src/relay/asset.rs +++ b/crates/swapper/src/relay/asset.rs @@ -37,11 +37,17 @@ pub static SUPPORTED_CHAINS: LazyLock> = LazyLock::new(|| vec![ SwapperChainAsset::Assets( Chain::Ethereum, - vec![AssetId::from_token(Chain::Ethereum, ETHEREUM_USDC_TOKEN_ID), AssetId::from_token(Chain::Ethereum, ETHEREUM_USDT_TOKEN_ID)], + vec![ + AssetId::from_token(Chain::Ethereum, ETHEREUM_USDC_TOKEN_ID), + AssetId::from_token(Chain::Ethereum, ETHEREUM_USDT_TOKEN_ID), + ], ), SwapperChainAsset::Assets( Chain::SmartChain, - vec![AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDC_TOKEN_ID), AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDT_TOKEN_ID)], + vec![ + AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDC_TOKEN_ID), + AssetId::from_token(Chain::SmartChain, SMARTCHAIN_USDT_TOKEN_ID), + ], ), SwapperChainAsset::Assets(Chain::Base, vec![AssetId::from_token(Chain::Base, BASE_USDC_TOKEN_ID)]), SwapperChainAsset::Assets(Chain::Arbitrum, vec![USDC_ARB_ASSET_ID.into(), USDT_ARB_ASSET_ID.into()]), @@ -49,7 +55,10 @@ pub static SUPPORTED_CHAINS: LazyLock> = LazyLock::new(|| SwapperChainAsset::Assets(Chain::Polygon, vec![USDC_POLYGON_ASSET_ID.into(), USDT_POLYGON_ASSET_ID.into()]), SwapperChainAsset::Assets( Chain::AvalancheC, - vec![AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDC_TOKEN_ID), AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDT_TOKEN_ID)], + vec![ + AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDC_TOKEN_ID), + AssetId::from_token(Chain::AvalancheC, AVALANCHE_USDT_TOKEN_ID), + ], ), SwapperChainAsset::Assets(Chain::Linea, vec![USDT_LINEA_ASSET_ID.into()]), SwapperChainAsset::Assets(Chain::ZkSync, vec![USDT_ZKSYNC_ASSET_ID.into()]), @@ -64,10 +73,15 @@ pub static SUPPORTED_CHAINS: LazyLock> = LazyLock::new(|| }); pub fn asset_to_currency(asset_id: &AssetId) -> Result { - if asset_id.is_native() { - Ok(EVM_ZERO_ADDRESS.to_string()) - } else { - asset_id.token_id.clone().ok_or(SwapperError::NotSupportedAsset) + match asset_id.chain.chain_type() { + ChainType::Ethereum => { + if asset_id.is_native() { + Ok(EVM_ZERO_ADDRESS.to_string()) + } else { + asset_id.token_id.clone().ok_or(SwapperError::NotSupportedAsset) + } + } + _ => Err(SwapperError::NotSupportedChain), } } @@ -88,4 +102,9 @@ mod tests { let result = asset_to_currency(&AssetId::from_token(Chain::Ethereum, token_address)).unwrap(); assert_eq!(result, token_address); } + + #[test] + fn test_non_evm_asset_not_supported() { + assert_eq!(asset_to_currency(&AssetId::from_chain(Chain::Solana)), Err(SwapperError::NotSupportedChain)); + } } diff --git a/crates/swapper/src/relay/chain.rs b/crates/swapper/src/relay/chain.rs index 525c0aa09..c92f0d999 100644 --- a/crates/swapper/src/relay/chain.rs +++ b/crates/swapper/src/relay/chain.rs @@ -1,48 +1,29 @@ use primitives::{Chain, chain_evm::EVMChain}; -const BITCOIN_CHAIN_ID: u64 = 8253038; -const SOLANA_CHAIN_ID: u64 = 792703809; - +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RelayChain { - Bitcoin, - Solana, Evm(EVMChain), } impl RelayChain { pub fn chain_id(&self) -> u64 { match self { - Self::Bitcoin => BITCOIN_CHAIN_ID, - Self::Solana => SOLANA_CHAIN_ID, - Self::Evm(evm_chain) => evm_chain.chain_id(), + Self::Evm(chain) => chain.chain_id(), } } pub fn from_chain(chain: &Chain) -> Option { - match chain { - Chain::Bitcoin => Some(Self::Bitcoin), - Chain::Solana => Some(Self::Solana), - _ => Some(Self::Evm(EVMChain::from_chain(*chain)?)), - } + Some(Self::Evm(EVMChain::from_chain(*chain)?)) } - pub fn to_chain(&self) -> Chain { + pub fn to_chain(self) -> Chain { match self { - Self::Bitcoin => Chain::Bitcoin, - Self::Solana => Chain::Solana, - Self::Evm(evm_chain) => evm_chain.to_chain(), + Self::Evm(chain) => chain.to_chain(), } } pub fn from_chain_id(chain_id: u64) -> Option { - match chain_id { - BITCOIN_CHAIN_ID => Some(Self::Bitcoin), - SOLANA_CHAIN_ID => Some(Self::Solana), - _ => { - let evm_chain = EVMChain::all().into_iter().find(|c| c.chain_id() == chain_id)?; - Some(Self::Evm(evm_chain)) - } - } + Some(Self::Evm(EVMChain::all().into_iter().find(|chain| chain.chain_id() == chain_id)?)) } } @@ -54,7 +35,8 @@ mod tests { fn test_from_chain() { assert_eq!(RelayChain::from_chain(&Chain::Ethereum).unwrap().chain_id(), EVMChain::Ethereum.chain_id()); assert_eq!(RelayChain::from_chain(&Chain::SmartChain).unwrap().chain_id(), EVMChain::SmartChain.chain_id()); - assert_eq!(RelayChain::from_chain(&Chain::Solana).unwrap().chain_id(), SOLANA_CHAIN_ID); + assert!(RelayChain::from_chain(&Chain::Solana).is_none()); + assert!(RelayChain::from_chain(&Chain::Bitcoin).is_none()); assert!(RelayChain::from_chain(&Chain::Cosmos).is_none()); } } diff --git a/crates/swapper/src/relay/mapper.rs b/crates/swapper/src/relay/mapper.rs index f52c79ca8..040496fda 100644 --- a/crates/swapper/src/relay/mapper.rs +++ b/crates/swapper/src/relay/mapper.rs @@ -1,30 +1,21 @@ use primitives::{TransactionSwapMetadata, swap::ApprovalData}; -use super::{DEFAULT_SWAP_GAS_LIMIT, asset::map_currency_to_asset_id, chain::RelayChain, model::{RelayRequest, Step, StepData}}; +use super::{ + DEFAULT_SWAP_GAS_LIMIT, + asset::map_currency_to_asset_id, + chain::RelayChain, + model::{RelayQuoteResponse, RelayRequest, StepData}, +}; use crate::{SwapResult, SwapperError, SwapperProvider, SwapperQuoteData}; -pub const STEP_SWAP: &str = "swap"; -pub const STEP_DEPOSIT: &str = "deposit"; -pub const STEP_APPROVE: &str = "approve"; - -pub fn get_step_data(steps: &[Step]) -> Result<&StepData, SwapperError> { - steps - .iter() - .find(|s| s.id == STEP_SWAP || s.id == STEP_DEPOSIT) - .or_else(|| steps.iter().find(|s| s.kind == "transaction" && s.id != STEP_APPROVE)) - .or_else(|| steps.iter().find(|s| s.step_data().is_some())) - .and_then(|s| s.step_data()) - .ok_or(SwapperError::InvalidRoute) -} - -pub fn map_quote_data(steps: &[Step], approval: Option) -> Result { - let step_data = get_step_data(steps)?; +pub fn map_quote_data(quote_response: &RelayQuoteResponse, approval: Option) -> Result { + let step_data = quote_response.step_data().ok_or(SwapperError::InvalidRoute)?; match step_data { StepData::Evm(evm) => { let gas_limit = approval.as_ref().map(|_| DEFAULT_SWAP_GAS_LIMIT.to_string()); - let data = evm.data.clone().unwrap_or_default(); - Ok(SwapperQuoteData::new_contract(evm.to.clone(), evm.value.clone(), data, approval, gas_limit)) + let call_data = evm.data.clone().unwrap_or_default(); + Ok(SwapperQuoteData::new_contract(evm.to.clone(), evm.value.clone(), call_data, approval, gas_limit)) } } } @@ -53,14 +44,22 @@ pub fn map_swap_result(request: &RelayRequest) -> SwapResult { #[cfg(test)] mod tests { use super::*; - use crate::relay::model::{RelayCurrencyDetail, RelayRequest, RelayRequestMetadata, RelayRequestsResponse, RelayStatus, Step}; + use crate::relay::model::{CurrencyAmount, QuoteDetails, RelayCurrencyDetail, RelayQuoteResponse, RelayRequest, RelayRequestMetadata, RelayStatus, Step}; use primitives::{AssetId, Chain, swap::SwapStatus}; #[test] fn test_map_evm_quote_data() { - let steps = vec![Step::mock_transaction("swap", "0xrouter", "1000000000000000000", "0xabcdef")]; + let quote_response = RelayQuoteResponse { + steps: vec![Step::mock_transaction("swap", "0xrouter", "1000000000000000000", "0xabcdef")], + details: QuoteDetails { + currency_out: CurrencyAmount { amount: "0".to_string() }, + time_estimate: None, + swap_impact: None, + }, + fees: None, + }; - let result = map_quote_data(&steps, None).unwrap(); + let result = map_quote_data("e_response, None).unwrap(); assert_eq!(result.to, "0xrouter"); assert_eq!(result.value, "1000000000000000000"); @@ -71,14 +70,22 @@ mod tests { #[test] fn test_map_evm_quote_data_with_approval() { - let steps = vec![Step::mock_transaction("swap", "0xrouter", "0", "0xabcdef")]; + let quote_response = RelayQuoteResponse { + steps: vec![Step::mock_transaction("swap", "0xrouter", "0", "0xabcdef")], + details: QuoteDetails { + currency_out: CurrencyAmount { amount: "0".to_string() }, + time_estimate: None, + swap_impact: None, + }, + fees: None, + }; let approval = ApprovalData { token: "0xtoken".to_string(), spender: "0xrouter".to_string(), value: "1000".to_string(), }; - let result = map_quote_data(&steps, Some(approval.clone())).unwrap(); + let result = map_quote_data("e_response, Some(approval.clone())).unwrap(); assert_eq!(result.to, "0xrouter"); assert_eq!(result.approval, Some(approval)); @@ -118,57 +125,18 @@ mod tests { } #[test] - fn test_map_swap_result_eth_to_btc() { - let response: RelayRequestsResponse = serde_json::from_str(include_str!("testdata/request_eth_to_btc.json")).unwrap(); - let request = response.requests.first().unwrap(); - let result = map_swap_result(request); - - assert_eq!(result.status, SwapStatus::Completed); - let metadata = result.metadata.unwrap(); - assert_eq!(metadata.from_asset, AssetId::from_chain(Chain::Ethereum)); - assert_eq!(metadata.from_value, "10000000000000000"); - assert_eq!(metadata.to_asset, AssetId::from_chain(Chain::Bitcoin)); - assert_eq!(metadata.to_value, "28619"); - assert_eq!(metadata.provider, Some("relay".to_string())); - } - - #[test] - fn test_map_swap_result_bsc_usdt_to_sol() { - let response: RelayRequestsResponse = serde_json::from_str(include_str!("testdata/request_bsc_usdt_to_sol.json")).unwrap(); - let request = response.requests.first().unwrap(); - let result = map_swap_result(request); - - assert_eq!(result.status, SwapStatus::Completed); - let metadata = result.metadata.unwrap(); - assert_eq!(metadata.from_asset, AssetId::from_token(Chain::SmartChain, "0x55d398326f99059fF775485246999027B3197955")); - assert_eq!(metadata.from_value, "6000000000000000000"); - assert_eq!(metadata.to_asset, AssetId::from_chain(Chain::Solana)); - assert_eq!(metadata.to_value, "74432990"); - } - - #[test] - fn test_get_step_data_by_id() { - let steps = vec![Step::mock_empty("approve", "transaction"), Step::mock_transaction("swap", "0xrouter", "0", "0xdata")]; - let data = get_step_data(&steps).unwrap(); - assert_eq!(data.get_to().as_deref(), Some("0xrouter")); - } - - #[test] - fn test_get_step_data_fallback_transaction_kind() { - let steps = vec![Step::mock_empty("approve", "transaction"), Step::mock_transaction("send", "0xto", "100", "0xdata")]; - let data = get_step_data(&steps).unwrap(); - assert_eq!(data.get_to().as_deref(), Some("0xto")); - } - - #[test] - fn test_get_step_data_empty_steps() { - assert!(get_step_data(&[]).is_err()); - } + fn test_map_quote_data_without_step_data() { + let quote_response = RelayQuoteResponse { + steps: vec![Step::mock_empty("approve", "transaction")], + details: QuoteDetails { + currency_out: CurrencyAmount { amount: "0".to_string() }, + time_estimate: None, + swap_impact: None, + }, + fees: None, + }; - #[test] - fn test_get_step_data_no_usable_steps() { - let steps = vec![Step::mock_empty("approve", "transaction")]; - assert!(get_step_data(&steps).is_err()); + assert!(map_quote_data("e_response, None).is_err()); } #[test] diff --git a/crates/swapper/src/relay/model.rs b/crates/swapper/src/relay/model.rs index c24b063f3..1dca9a3f4 100644 --- a/crates/swapper/src/relay/model.rs +++ b/crates/swapper/src/relay/model.rs @@ -1,7 +1,12 @@ +use std::collections::BTreeSet; + use gem_evm::address::ethereum_address_checksum; use primitives::swap::{SwapMode, SwapStatus}; use serde::{Deserialize, Serialize}; -use serde_serializers::deserialize_string_from_value; + +const STEP_SWAP: &str = "swap"; +const STEP_DEPOSIT: &str = "deposit"; +const STEP_APPROVE: &str = "approve"; pub fn relay_trade_type(mode: &SwapMode) -> &'static str { match mode { @@ -21,8 +26,7 @@ pub struct RelayQuoteRequest { pub amount: String, pub recipient: String, pub trade_type: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub refund_to: Option, + pub refund_to: String, #[serde(skip_serializing_if = "Option::is_none")] pub referrer: Option, #[serde(skip_serializing_if = "Vec::is_empty")] @@ -36,14 +40,6 @@ pub struct RelayAppFee { pub fee: String, } -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RelayQuoteResponse { - pub steps: Vec, - pub details: QuoteDetails, - pub fees: Option, -} - #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct RelayFees { @@ -56,6 +52,29 @@ pub struct RelayFeeAmount { pub amount: Option, } +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RelayQuoteResponse { + pub steps: Vec, + pub details: QuoteDetails, + pub fees: Option, +} + +impl RelayQuoteResponse { + pub fn step_data(&self) -> Option<&StepData> { + self.steps + .iter() + .find(|step| step.id == STEP_SWAP || step.id == STEP_DEPOSIT) + .or_else(|| self.steps.iter().find(|step| step.kind == "transaction" && step.id != STEP_APPROVE)) + .or_else(|| self.steps.iter().find(|step| step.step_data().is_some())) + .and_then(Step::step_data) + } + + pub fn router_address(&self) -> Option { + self.steps.iter().filter(|step| step.id != STEP_APPROVE).find_map(Step::to_address) + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Step { @@ -68,6 +87,10 @@ impl Step { pub fn step_data(&self) -> Option<&StepData> { self.items.as_ref()?.first()?.data.as_ref() } + + pub fn to_address(&self) -> Option { + Some(self.step_data()?.to_address()) + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -83,9 +106,9 @@ pub enum StepData { } impl StepData { - pub fn get_to(&self) -> Option { + pub fn to_address(&self) -> String { match self { - Self::Evm(evm) => Some(evm.to.clone()), + Self::Evm(evm) => evm.to.clone(), } } } @@ -95,7 +118,6 @@ impl StepData { pub struct EvmStepData { pub to: String, pub data: Option, - #[serde(default, deserialize_with = "deserialize_string_from_value")] pub value: String, } @@ -209,16 +231,91 @@ pub struct RelayChainsResponse { pub struct RelayChainInfo { #[serde(default)] pub solver_addresses: Vec, + pub protocol: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RelayProtocol { + pub v2: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RelayProtocolV2 { + pub depository: String, } impl RelayChainsResponse { - pub fn solver_addresses(&self) -> Vec { + pub fn deposit_addresses(&self) -> Vec { self.chains .iter() - .flat_map(|c| &c.solver_addresses) - .map(|addr| ethereum_address_checksum(addr).unwrap_or_else(|_| addr.clone())) - .collect::>() + .filter_map(|chain| chain.protocol.as_ref()?.v2.as_ref()) + .map(|protocol| protocol.depository.clone()) + .map(|address| ethereum_address_checksum(&address).unwrap_or(address)) + .collect::>() .into_iter() .collect() } + + pub fn send_addresses(&self) -> Vec { + self.chains + .iter() + .flat_map(|chain| chain.solver_addresses.iter()) + .map(|address| ethereum_address_checksum(address).unwrap_or_else(|_| address.clone())) + .collect::>() + .into_iter() + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deposit_addresses() { + let depository = "0x4cd00e387622c35bddb9b4c962c136462338bc31"; + let response = RelayChainsResponse { + chains: vec![ + RelayChainInfo { + solver_addresses: vec![], + protocol: Some(RelayProtocol { + v2: Some(RelayProtocolV2 { + depository: depository.to_string(), + }), + }), + }, + RelayChainInfo { + solver_addresses: vec![], + protocol: Some(RelayProtocol { + v2: Some(RelayProtocolV2 { + depository: "0x59916da825d2d2ec1bf878d71c88826f6633ecca".to_string(), + }), + }), + }, + ], + }; + + assert_eq!( + response.deposit_addresses(), + vec![ + ethereum_address_checksum(depository).unwrap(), + ethereum_address_checksum("0x59916da825d2d2ec1bf878d71c88826f6633ecca").unwrap(), + ] + ); + } + + #[test] + fn test_send_addresses() { + let solver = "0xf70da97812cb96acdf810712aa562db8dfa3dbef"; + let response = RelayChainsResponse { + chains: vec![RelayChainInfo { + solver_addresses: vec![solver.to_string(), solver.to_string()], + protocol: None, + }], + }; + + assert_eq!(response.send_addresses(), vec![ethereum_address_checksum(solver).unwrap()]); + } } diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs index f3c3061d1..da56e263a 100644 --- a/crates/swapper/src/relay/provider.rs +++ b/crates/swapper/src/relay/provider.rs @@ -14,7 +14,7 @@ use super::{ }; use crate::{ FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, RpcClient, RpcProvider, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, - SwapperQuoteData, approval::check_approval_erc20, config::get_swap_api_url, cross_chain::VaultAddresses, fees::resolve_max_quote_amount, referrer::DEFAULT_REFERRER, + SwapperQuoteData, approval::check_approval_erc20, config::get_swap_api_url, cross_chain::VaultAddresses, fees::resolve_max_quote_value, referrer::DEFAULT_REFERRER, }; #[derive(Debug)] @@ -72,7 +72,7 @@ where let origin_currency = asset_to_currency(&from_asset_id)?; let destination_currency = asset_to_currency(&to_asset_id)?; let app_fees = resolve_app_fees(request); - let amount = resolve_max_quote_amount(request)?; + let from_value = resolve_max_quote_value(request)?; let relay_request = RelayQuoteRequest { user: request.wallet_address.clone(), @@ -80,12 +80,12 @@ where destination_chain_id: to_chain.chain_id(), origin_currency, destination_currency, - amount: amount.clone(), + amount: from_value.clone(), recipient: request.destination_address.clone(), trade_type: relay_trade_type(&request.mode).to_string(), referrer: if app_fees.is_empty() { None } else { Some(DEFAULT_REFERRER.to_string()) }, app_fees, - refund_to: Some(request.wallet_address.clone()), + refund_to: request.wallet_address.clone(), max_route_length: 6, }; @@ -95,7 +95,7 @@ where let eta_in_seconds = quote_response.details.time_estimate_u32(); let quote = Quote { - from_value: amount, + from_value, to_value, data: ProviderData { provider: self.provider().clone(), @@ -120,7 +120,7 @@ where let from_asset_id = quote.request.from_asset.asset_id(); let approval = self.check_evm_approval(quote, "e_response, &from_asset_id).await?; - mapper::map_quote_data("e_response.steps, approval) + mapper::map_quote_data("e_response, approval) } async fn get_swap_result(&self, _chain: Chain, transaction_hash: &str) -> Result { @@ -131,10 +131,9 @@ where async fn get_vault_addresses(&self, _from_timestamp: Option) -> Result { let response = self.client.get_chains().await?; - let addresses = response.solver_addresses(); Ok(VaultAddresses { - deposit: addresses.clone(), - send: addresses, + deposit: response.deposit_addresses(), + send: response.send_addresses(), }) } } @@ -146,12 +145,7 @@ where async fn check_evm_approval(&self, quote: &Quote, quote_response: &RelayQuoteResponse, from_asset_id: &AssetId) -> Result, SwapperError> { match from_asset_id.chain.chain_type() { ChainType::Ethereum if !from_asset_id.is_native() => { - let router_address = quote_response - .steps - .iter() - .filter(|s| s.id != mapper::STEP_APPROVE) - .find_map(|s| s.step_data()?.get_to()) - .ok_or(SwapperError::InvalidRoute)?; + let spender = quote_response.router_address().ok_or(SwapperError::InvalidRoute)?; let token = from_asset_id.token_id.clone().ok_or(SwapperError::NotSupportedAsset)?; let amount: U256 = quote.from_value.parse().map_err(SwapperError::from)?; @@ -159,7 +153,7 @@ where Ok(check_approval_erc20( quote.request.wallet_address.clone(), token, - router_address, + spender, amount, self.rpc_provider.clone(), &from_asset_id.chain, diff --git a/crates/swapper/src/relay/testdata/request_bsc_usdt_to_sol.json b/crates/swapper/src/relay/testdata/request_bsc_usdt_to_sol.json deleted file mode 100644 index f80af330f..000000000 --- a/crates/swapper/src/relay/testdata/request_bsc_usdt_to_sol.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "requests": [ - { - "id": "0xd2bcbf6c8e155411f6633067a29b1ee511d3d98b25db49393dd4ce58b00ee0f8", - "status": "success", - "data": { - "metadata": { - "sender": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", - "recipient": "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC", - "currencyIn": { - "currency": { - "chainId": 56, - "address": "0x55d398326f99059ff775485246999027b3197955", - "symbol": "USDT", - "name": "Tether USD", - "decimals": 18 - }, - "amount": "6000000000000000000", - "amountFormatted": "6.0", - "amountUsd": "6.000000" - }, - "currencyOut": { - "currency": { - "chainId": 792703809, - "address": "So11111111111111111111111111111111111111112", - "symbol": "WSOL", - "name": "Wrapped SOL", - "decimals": 9 - }, - "amount": "74432990", - "amountFormatted": "0.07443299", - "amountUsd": "5.806518" - } - } - } - } - ] -} diff --git a/crates/swapper/src/relay/testdata/request_eth_to_btc.json b/crates/swapper/src/relay/testdata/request_eth_to_btc.json deleted file mode 100644 index 3a20d33a8..000000000 --- a/crates/swapper/src/relay/testdata/request_eth_to_btc.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "requests": [ - { - "id": "0x66dfacf8279d88615a013041f7bb19e7a634b367e6d80d54c96b33339a55078f", - "status": "success", - "user": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", - "recipient": "bc1qe7qlndxgfv76c0ulnfhh7j0vdwkqdkkl4yf9gm", - "data": { - "metadata": { - "sender": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", - "recipient": "bc1qe7qlndxgfv76c0ulnfhh7j0vdwkqdkkl4yf9gm", - "currencyIn": { - "currency": { - "chainId": 1, - "address": "0x0000000000000000000000000000000000000000", - "symbol": "ETH", - "name": "Ether", - "decimals": 18 - }, - "amount": "10000000000000000", - "amountFormatted": "0.01", - "amountUsd": "19.937560" - }, - "currencyOut": { - "currency": { - "chainId": 8253038, - "address": "bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8", - "symbol": "BTC", - "name": "Bitcoin", - "decimals": 8 - }, - "amount": "28619", - "amountFormatted": "0.00028619", - "amountUsd": "19.445550" - } - } - }, - "createdAt": "2026-03-03T06:28:25.979Z", - "updatedAt": "2026-03-03T07:11:36.416Z" - } - ] -} From e97fd393514620f151bdd5fb1c740a199d049130 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:53:50 +0900 Subject: [PATCH 12/20] Update mapper.rs --- crates/swapper/src/relay/mapper.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/swapper/src/relay/mapper.rs b/crates/swapper/src/relay/mapper.rs index 040496fda..31f20fbef 100644 --- a/crates/swapper/src/relay/mapper.rs +++ b/crates/swapper/src/relay/mapper.rs @@ -44,7 +44,9 @@ pub fn map_swap_result(request: &RelayRequest) -> SwapResult { #[cfg(test)] mod tests { use super::*; - use crate::relay::model::{CurrencyAmount, QuoteDetails, RelayCurrencyDetail, RelayQuoteResponse, RelayRequest, RelayRequestMetadata, RelayStatus, Step}; + use crate::relay::model::{ + CurrencyAmount, QuoteDetails, RelayCurrencyDetail, RelayQuoteResponse, RelayRequest, RelayRequestMetadata, RelayRequestsResponse, RelayStatus, Step, + }; use primitives::{AssetId, Chain, swap::SwapStatus}; #[test] From 67d04debca5009363c8d9e952cea76299265bd76 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:59:33 +0900 Subject: [PATCH 13/20] Update mapper.rs --- crates/swapper/src/relay/mapper.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/swapper/src/relay/mapper.rs b/crates/swapper/src/relay/mapper.rs index 31f20fbef..9a5422ae7 100644 --- a/crates/swapper/src/relay/mapper.rs +++ b/crates/swapper/src/relay/mapper.rs @@ -156,16 +156,12 @@ mod tests { } #[test] - fn test_map_swap_result_base_usdc_to_sol_usdt() { + fn test_map_swap_result_base_usdc_to_sol_usdt_without_metadata() { let response: RelayRequestsResponse = serde_json::from_str(include_str!("testdata/request_base_usdc_to_sol_usdt.json")).unwrap(); let request = response.requests.first().unwrap(); let result = map_swap_result(request); assert_eq!(result.status, SwapStatus::Completed); - let metadata = result.metadata.unwrap(); - assert_eq!(metadata.from_asset, AssetId::from_token(Chain::Base, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")); - assert_eq!(metadata.from_value, "3000000"); - assert_eq!(metadata.to_asset, AssetId::from_token(Chain::Solana, "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB")); - assert_eq!(metadata.to_value, "2960498"); + assert!(result.metadata.is_none()); } } From a39ce1cec2a9fdf4b2916f96fcccf237b3a188c1 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:12:03 +0900 Subject: [PATCH 14/20] more fetch -> get --- crates/swapper/src/across/provider.rs | 12 ++++++------ crates/swapper/src/chainflip/provider.rs | 4 ++-- crates/swapper/src/hyperliquid/provider/bridge.rs | 4 ++-- .../src/hyperliquid/provider/hyperliquid.rs | 12 ++++++------ .../src/hyperliquid/provider/spot/provider.rs | 8 ++++---- crates/swapper/src/jupiter/provider.rs | 8 ++++---- crates/swapper/src/near_intents/provider.rs | 12 ++++++------ crates/swapper/src/proxy/provider.rs | 14 +++++++------- crates/swapper/src/relay/provider.rs | 12 ++++++------ crates/swapper/src/swapper.rs | 12 ++++++------ crates/swapper/src/swapper_trait.rs | 6 +++--- crates/swapper/src/testkit.rs | 4 ++-- crates/swapper/src/thorchain/provider.rs | 10 +++++----- crates/swapper/src/uniswap/v3/provider.rs | 6 +++--- crates/swapper/src/uniswap/v4/provider.rs | 10 +++++----- gemstone/src/gem_swapper/mod.rs | 4 ++-- 16 files changed, 69 insertions(+), 69 deletions(-) diff --git a/crates/swapper/src/across/provider.rs b/crates/swapper/src/across/provider.rs index 964905f41..0e38dded3 100644 --- a/crates/swapper/src/across/provider.rs +++ b/crates/swapper/src/across/provider.rs @@ -362,7 +362,7 @@ impl Swapper for Across { ] } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { if request.from_asset.chain() == request.to_asset.chain() { return Err(SwapperError::NoQuoteAvailable); } @@ -506,7 +506,7 @@ impl Swapper for Across { }) } - async fn fetch_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { let from_chain = quote.request.from_asset.chain(); let deployment = AcrossDeployment::deployment_by_chain(&from_chain).ok_or(SwapperError::NotSupportedChain)?; let dst_chain_id: u32 = quote.request.to_asset.chain().network_id().parse().unwrap(); @@ -835,14 +835,14 @@ mod tests { }; let now = SystemTime::now(); - let quote = swap_provider.fetch_quote(&request).await?; + let quote = swap_provider.get_quote(&request).await?; let elapsed = SystemTime::now().duration_since(now).unwrap(); println!("<== elapsed: {:?}", elapsed); println!("<== quote: {:?}", quote); assert!(quote.to_value.parse::().unwrap() > 0); - let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::EstimateGas).await?; + let quote_data = swap_provider.get_quote_data("e, FetchQuoteData::EstimateGas).await?; println!("<== quote_data: {:?}", quote_data); Ok(()) @@ -873,14 +873,14 @@ mod tests { }; let now = SystemTime::now(); - let quote = swap_provider.fetch_quote(&request).await?; + let quote = swap_provider.get_quote(&request).await?; let elapsed = SystemTime::now().duration_since(now).unwrap(); println!("<== elapsed: {:?}", elapsed); println!("<== quote: {:?}", quote); assert!(quote.to_value.parse::().unwrap() > 0); - let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::EstimateGas).await?; + let quote_data = swap_provider.get_quote_data("e, FetchQuoteData::EstimateGas).await?; println!("<== quote_data: {:?}", quote_data); Ok(()) diff --git a/crates/swapper/src/chainflip/provider.rs b/crates/swapper/src/chainflip/provider.rs index 6d46992d5..311901d79 100644 --- a/crates/swapper/src/chainflip/provider.rs +++ b/crates/swapper/src/chainflip/provider.rs @@ -148,7 +148,7 @@ where ] } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { if request.from_asset.chain().chain_type() == ChainType::Bitcoin { return Err(SwapperError::NoQuoteAvailable); } @@ -196,7 +196,7 @@ where }) } - async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let from_asset = quote.request.from_asset.asset_id(); let source_asset = Self::map_asset_id("e.request.from_asset); let destination_asset = Self::map_asset_id("e.request.to_asset); diff --git a/crates/swapper/src/hyperliquid/provider/bridge.rs b/crates/swapper/src/hyperliquid/provider/bridge.rs index 0f9318c8b..061cdeef1 100644 --- a/crates/swapper/src/hyperliquid/provider/bridge.rs +++ b/crates/swapper/src/hyperliquid/provider/bridge.rs @@ -48,7 +48,7 @@ impl Swapper for HyperCoreBridge { ] } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { let to_value = scale_quote_value(&request.value, request.from_asset.decimals, request.to_asset.decimals)?; let quote = Quote { @@ -71,7 +71,7 @@ impl Swapper for HyperCoreBridge { Ok(quote) } - async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { match quote.request.from_asset.asset_id().chain { Chain::HyperCore => { let decimals: i32 = quote.request.from_asset.decimals.try_into().unwrap(); diff --git a/crates/swapper/src/hyperliquid/provider/hyperliquid.rs b/crates/swapper/src/hyperliquid/provider/hyperliquid.rs index e522d4ef6..aa39acd4b 100644 --- a/crates/swapper/src/hyperliquid/provider/hyperliquid.rs +++ b/crates/swapper/src/hyperliquid/provider/hyperliquid.rs @@ -54,24 +54,24 @@ impl Swapper for Hyperliquid { assets } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { if Self::is_spot_request(request) { - return self.spot.fetch_quote(request).await; + return self.spot.get_quote(request).await; } if Self::is_bridge_request(request) { - return self.bridge.fetch_quote(request).await; + return self.bridge.get_quote(request).await; } Err(SwapperError::NoQuoteAvailable) } - async fn fetch_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { if Self::is_spot_request("e.request) { - return self.spot.fetch_quote_data(quote, data).await; + return self.spot.get_quote_data(quote, data).await; } if Self::is_bridge_request("e.request) { - return self.bridge.fetch_quote_data(quote, data).await; + return self.bridge.get_quote_data(quote, data).await; } Err(SwapperError::NoQuoteAvailable) } diff --git a/crates/swapper/src/hyperliquid/provider/spot/provider.rs b/crates/swapper/src/hyperliquid/provider/spot/provider.rs index a85436c85..7a6c27250 100644 --- a/crates/swapper/src/hyperliquid/provider/spot/provider.rs +++ b/crates/swapper/src/hyperliquid/provider/spot/provider.rs @@ -130,7 +130,7 @@ impl Swapper for HyperCoreSpot { )] } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { let client = self.client()?; let meta = self.load_spot_meta().await?; let from_token = self.resolve_token(&meta, &request.from_asset)?; @@ -239,7 +239,7 @@ impl Swapper for HyperCoreSpot { Ok(quote) } - async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + async fn get_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::TransactionError(err.to_string()))?; @@ -270,13 +270,13 @@ mod tests { request.options.preferred_providers = vec![SwapperProvider::Hyperliquid]; request.value = "2000000000".into(); - let quote = spot.fetch_quote(&request).await.unwrap(); + let quote = spot.get_quote(&request).await.unwrap(); let order: PlaceOrder = serde_json::from_str("e.data.routes[0].route_data).unwrap(); assert_eq!(order.r#type, "order"); assert!(order.orders[0].asset >= SPOT_ASSET_OFFSET); - let quote_data = spot.fetch_quote_data("e, FetchQuoteData::None).await.unwrap(); + let quote_data = spot.get_quote_data("e, FetchQuoteData::None).await.unwrap(); assert_eq!(quote.data.provider.id, SwapperProvider::Hyperliquid); assert!(!quote.to_value.is_empty()); assert!(matches!(quote_data.data_type, SwapQuoteDataType::Contract)); diff --git a/crates/swapper/src/jupiter/provider.rs b/crates/swapper/src/jupiter/provider.rs index 323e240ca..fb475822a 100644 --- a/crates/swapper/src/jupiter/provider.rs +++ b/crates/swapper/src/jupiter/provider.rs @@ -122,7 +122,7 @@ where vec![SwapperChainAsset::All(Chain::Solana)] } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { let input_mint = self.get_asset_address(&request.from_asset.id)?; let output_mint = self.get_asset_address(&request.to_asset.id)?; let swap_options = request.options.clone(); @@ -162,7 +162,7 @@ where Ok(quote) } - async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { if quote.data.routes.is_empty() { return Err(SwapperError::InvalidRoute); } @@ -220,7 +220,7 @@ mod swap_integration_tests { options, }; - let quote = provider.fetch_quote(&request).await?; + let quote = provider.get_quote(&request).await?; assert_eq!(quote.from_value, request.value); assert!(quote.to_value.parse::().unwrap() > 0); @@ -236,7 +236,7 @@ mod swap_integration_tests { assert_eq!(quote_response.input_mint, "So11111111111111111111111111111111111111112"); assert_eq!(quote_response.output_mint, USDC_TOKEN_MINT); - let quote_data = provider.fetch_quote_data("e, FetchQuoteData::None).await?; + let quote_data = provider.get_quote_data("e, FetchQuoteData::None).await?; assert_eq!(quote_data.to, PROGRAM_ADDRESS); assert!(!quote_data.data.is_empty()); diff --git a/crates/swapper/src/near_intents/provider.rs b/crates/swapper/src/near_intents/provider.rs index 8f3608fc8..5679a7e3c 100644 --- a/crates/swapper/src/near_intents/provider.rs +++ b/crates/swapper/src/near_intents/provider.rs @@ -282,7 +282,7 @@ where self.supported_assets.clone() } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { let mode = match request.mode { SwapperMode::ExactIn => SwapType::FlexInput, SwapperMode::ExactOut => return Err(SwapperError::NotSupportedAsset), @@ -314,7 +314,7 @@ where }) } - async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let route = quote.data.routes.first().ok_or(SwapperError::InvalidRoute)?; let mut quote_request: NearQuoteRequest = serde_json::from_str(&route.route_data)?; let request_deposit_mode = quote_request.deposit_mode.clone(); @@ -587,10 +587,10 @@ mod swap_integration_tests { options, }; - let quote = provider.fetch_quote(&request).await?; + let quote = provider.get_quote(&request).await?; assert!(!quote.to_value.is_empty()); - let quote_data = provider.fetch_quote_data("e, FetchQuoteData::None).await?; + let quote_data = provider.get_quote_data("e, FetchQuoteData::None).await?; assert!(!quote_data.to.is_empty()); Ok(()) @@ -616,12 +616,12 @@ mod swap_integration_tests { options, }; - let quote = match provider.fetch_quote(&request).await { + let quote = match provider.get_quote(&request).await { Ok(quote) => quote, Err(SwapperError::ComputeQuoteError(_)) => return Ok(()), Err(error) => return Err(error), }; - let quote_data = match provider.fetch_quote_data("e, FetchQuoteData::None).await { + let quote_data = match provider.get_quote_data("e, FetchQuoteData::None).await { Ok(data) => data, Err(SwapperError::TransactionError(_)) => return Ok(()), Err(error) => return Err(error), diff --git a/crates/swapper/src/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs index e4bec20f4..9c842a3dc 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -186,7 +186,7 @@ where self.assets.clone() } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { let quote_request = ProxyQuoteRequest { from_address: request.wallet_address.clone(), to_address: request.destination_address.clone(), @@ -218,7 +218,7 @@ where }) } - async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let route = quote.data.routes.first().ok_or(SwapperError::InvalidRoute)?; let route_data: ProxyQuote = serde_json::from_str(&route.route_data).map_err(|_| SwapperError::InvalidRoute)?; @@ -362,7 +362,7 @@ mod swap_integration_tests { options, }; - let quote = provider.fetch_quote(&request).await?; + let quote = provider.get_quote(&request).await?; assert_eq!(quote.from_value, request.value); assert!(quote.to_value.parse::().unwrap() > 0); @@ -396,7 +396,7 @@ mod swap_integration_tests { options, }; - let quote = provider.fetch_quote(&request).await?; + let quote = provider.get_quote(&request).await?; assert_eq!(quote.from_value, request.value); assert!(quote.to_value.parse::().unwrap() > 0); @@ -449,7 +449,7 @@ mod swap_integration_tests { options, }; - let result = provider.fetch_quote(&request).await; + let result = provider.get_quote(&request).await; assert!(result.is_err(), "Expected error for tiny swap amount"); let err = result.unwrap_err(); @@ -479,12 +479,12 @@ mod swap_integration_tests { options, }; - let quote = provider.fetch_quote(&request).await?; + let quote = provider.get_quote(&request).await?; assert!(quote.to_value.parse::().unwrap() > 0); println!("Quote: from={} to={}", quote.from_value, quote.to_value); - let quote_data = provider.fetch_quote_data("e, FetchQuoteData::None).await?; + let quote_data = provider.get_quote_data("e, FetchQuoteData::None).await?; assert!(!quote_data.to.is_empty(), "Expected non-empty 'to' address"); assert!(!quote_data.data.is_empty(), "Expected non-empty calldata"); diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs index da56e263a..11dbedd53 100644 --- a/crates/swapper/src/relay/provider.rs +++ b/crates/swapper/src/relay/provider.rs @@ -62,7 +62,7 @@ where SUPPORTED_CHAINS.clone() } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { let from_chain = RelayChain::from_chain(&request.from_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; let to_chain = RelayChain::from_chain(&request.to_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; @@ -114,7 +114,7 @@ where Ok(quote) } - async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let route = quote.data.routes.first().ok_or(SwapperError::InvalidRoute)?; let quote_response: RelayQuoteResponse = serde_json::from_str(&route.route_data).map_err(|_| SwapperError::InvalidRoute)?; @@ -189,8 +189,8 @@ mod swap_integration_tests { options: Options::new_with_slippage(100.into()), }; - let quote = relay.fetch_quote(&request).await?; - let quote_data = relay.fetch_quote_data("e, FetchQuoteData::None).await?; + let quote = relay.get_quote(&request).await?; + let quote_data = relay.get_quote_data("e, FetchQuoteData::None).await?; println!("quote: from_value={}, to_value={}", quote.from_value, quote.to_value); println!("quote_data: to={}, value={}, data_len={}", quote_data.to, quote_data.value, quote_data.data.len()); @@ -219,8 +219,8 @@ mod swap_integration_tests { options: Options::new_with_slippage(100.into()), }; - let quote = relay.fetch_quote(&request).await?; - let quote_data = relay.fetch_quote_data("e, FetchQuoteData::None).await?; + let quote = relay.get_quote(&request).await?; + let quote_data = relay.get_quote_data("e, FetchQuoteData::None).await?; println!("quote: from_value={}, to_value={}", quote.from_value, quote.to_value); println!("quote_data: to={}, value={}, data_len={}", quote_data.to, quote_data.value, quote_data.data.len()); diff --git a/crates/swapper/src/swapper.rs b/crates/swapper/src/swapper.rs index a83ff47da..4cdd5d657 100644 --- a/crates/swapper/src/swapper.rs +++ b/crates/swapper/src/swapper.rs @@ -193,7 +193,7 @@ impl GemSwapper { let providers = self.swappers.iter().filter(|x| provider_ids.contains(&x.provider().id)).collect::>(); 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 = providers.into_iter().map(|x| x.get_quote(request_for_quote.as_ref())); let quote_results = futures::future::join_all(quotes_futures).await; @@ -219,20 +219,20 @@ impl GemSwapper { Ok(quotes) } - pub async fn fetch_quote_by_provider(&self, provider: SwapperProvider, request: QuoteRequest) -> Result { + pub async fn get_quote_by_provider(&self, provider: SwapperProvider, request: QuoteRequest) -> Result { let provider = self.get_swapper_by_provider(&provider)?; let request_for_quote = Self::transform_request(&request); - provider.fetch_quote(request_for_quote.as_ref()).await + provider.get_quote(request_for_quote.as_ref()).await } - pub async fn fetch_permit2_for_quote(&self, quote: &Quote) -> Result, SwapperError> { + pub async fn get_permit2_for_quote(&self, quote: &Quote) -> Result, SwapperError> { let provider = self.get_swapper_by_provider("e.data.provider.id)?; - provider.fetch_permit2_for_quote(quote).await + provider.get_permit2_for_quote(quote).await } pub async fn get_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { let provider = self.get_swapper_by_provider("e.data.provider.id)?; - let mut quote_data = provider.fetch_quote_data(quote, data).await?; + let mut quote_data = provider.get_quote_data(quote, data).await?; if let Some(gas_limit) = quote_data.gas_limit.take() { quote_data.gas_limit = Some(Self::apply_gas_limit_multiplier("e.request.from_asset.chain(), gas_limit)); } diff --git a/crates/swapper/src/swapper_trait.rs b/crates/swapper/src/swapper_trait.rs index 55bcd173e..d11f807be 100644 --- a/crates/swapper/src/swapper_trait.rs +++ b/crates/swapper/src/swapper_trait.rs @@ -13,11 +13,11 @@ use primitives::{Chain, swap::SwapStatus}; pub trait Swapper: Send + Sync + Debug { fn provider(&self) -> &ProviderType; fn supported_assets(&self) -> Vec; - async fn fetch_quote(&self, request: &QuoteRequest) -> Result; - async fn fetch_permit2_for_quote(&self, _quote: &Quote) -> Result, SwapperError> { + async fn get_quote(&self, request: &QuoteRequest) -> Result; + async fn get_permit2_for_quote(&self, _quote: &Quote) -> Result, SwapperError> { Ok(None) } - async fn fetch_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result; + async fn get_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result; async fn get_vault_addresses(&self, _from_timestamp: Option) -> Result { Ok(VaultAddresses { deposit: vec![], send: vec![] }) } diff --git a/crates/swapper/src/testkit.rs b/crates/swapper/src/testkit.rs index 83b0cca1f..d70ef81a8 100644 --- a/crates/swapper/src/testkit.rs +++ b/crates/swapper/src/testkit.rs @@ -94,11 +94,11 @@ impl Swapper for MockSwapper { self.supported_assets.clone() } - async fn fetch_quote(&self, _request: &QuoteRequest) -> Result { + async fn get_quote(&self, _request: &QuoteRequest) -> Result { (self.response)() } - async fn fetch_quote_data(&self, _quote: &Quote, _data: FetchQuoteData) -> Result { + async fn get_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 cdd667338..97f27f451 100644 --- a/crates/swapper/src/thorchain/provider.rs +++ b/crates/swapper/src/thorchain/provider.rs @@ -87,7 +87,7 @@ where Ok(VaultAddresses { deposit, send }) } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { let from_asset = THORChainAsset::from_asset_id(&request.from_asset.id).ok_or(SwapperError::NotSupportedAsset)?; let to_asset = THORChainAsset::from_asset_id(&request.to_asset.id).ok_or(SwapperError::NotSupportedAsset)?; @@ -151,7 +151,7 @@ where Ok(quote) } - async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let fee = quote.request.options.clone().fee.unwrap_or_default().thorchain; let from_asset = THORChainAsset::from_asset_id("e.request.from_asset.id).ok_or(SwapperError::NotSupportedAsset)?; let to_asset = THORChainAsset::from_asset_id("e.request.to_asset.id).ok_or(SwapperError::NotSupportedAsset)?; @@ -233,7 +233,7 @@ mod swap_integration_tests { let to_asset = SwapperQuoteAsset::from(Chain::SmartChain.as_asset_id()); let request = mock_quote(from_asset, to_asset); - let quote = swapper.fetch_quote(&request).await?; + let quote = swapper.get_quote(&request).await?; assert_eq!(quote.from_value, request.value); assert!(quote.to_value.parse::().unwrap() > 0); @@ -253,7 +253,7 @@ mod swap_integration_tests { let mut request = mock_quote(from_asset, to_asset); request.value = "100000000".to_string(); - let quote = swapper.fetch_quote(&request).await?; + let quote = swapper.get_quote(&request).await?; assert_eq!(quote.from_value, request.value); assert!(quote.to_value.parse::().unwrap() > 0); @@ -273,7 +273,7 @@ mod swap_integration_tests { let mut request = mock_quote(from_asset, to_asset); request.value = "1".to_string(); - let err = swapper.fetch_quote(&request).await.expect_err("expected error"); + let err = swapper.get_quote(&request).await.expect_err("expected error"); assert!(matches!(err, SwapperError::InputAmountError { .. })); Ok(()) diff --git a/crates/swapper/src/uniswap/v3/provider.rs b/crates/swapper/src/uniswap/v3/provider.rs index 36cd3f9f3..58f7b0f43 100644 --- a/crates/swapper/src/uniswap/v3/provider.rs +++ b/crates/swapper/src/uniswap/v3/provider.rs @@ -110,7 +110,7 @@ impl Swapper for UniswapV3 { Chain::all().iter().filter(|x| self.support_chain(x)).map(|x| SwapperChainAsset::All(*x)).collect() } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { let from_chain = request.from_asset.chain(); let to_chain = request.to_asset.chain(); let deployment = self.provider.get_deployment_by_chain(&from_chain).ok_or(SwapperError::NotSupportedChain)?; @@ -191,7 +191,7 @@ impl Swapper for UniswapV3 { }) } - async fn fetch_permit2_for_quote(&self, quote: &Quote) -> Result, SwapperError> { + async fn get_permit2_for_quote(&self, quote: &Quote) -> Result, SwapperError> { let from_asset = quote.request.from_asset.asset_id(); if from_asset.is_native() { return Ok(None); @@ -203,7 +203,7 @@ impl Swapper for UniswapV3 { .await } - async fn fetch_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { let request = "e.request; let from_chain = request.from_asset.chain(); let (_, token_in, token_out, amount_in) = Self::parse_request(request)?; diff --git a/crates/swapper/src/uniswap/v4/provider.rs b/crates/swapper/src/uniswap/v4/provider.rs index 3f5d418ee..255046eeb 100644 --- a/crates/swapper/src/uniswap/v4/provider.rs +++ b/crates/swapper/src/uniswap/v4/provider.rs @@ -103,7 +103,7 @@ impl Swapper for UniswapV4 { Chain::all().iter().filter(|x| self.support_chain(x)).map(|x| SwapperChainAsset::All(*x)).collect() } - async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + async fn get_quote(&self, request: &QuoteRequest) -> Result { let from_chain = request.from_asset.chain(); let to_chain = request.to_asset.chain(); let deployment = get_uniswap_deployment_by_chain(&from_chain).ok_or(SwapperError::NotSupportedChain)?; @@ -186,7 +186,7 @@ impl Swapper for UniswapV4 { }) } - async fn fetch_permit2_for_quote(&self, quote: &Quote) -> Result, SwapperError> { + async fn get_permit2_for_quote(&self, quote: &Quote) -> Result, SwapperError> { let from_asset = quote.request.from_asset.asset_id(); if from_asset.is_native() { return Ok(None); @@ -209,7 +209,7 @@ impl Swapper for UniswapV4 { Ok(permit2_data) } - async fn fetch_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { + async fn get_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { let request = "e.request; let from_asset = request.from_asset.asset_id(); let (_, token_in, token_out, amount_in) = Self::parse_request(request)?; @@ -331,14 +331,14 @@ mod tests { }; let now = SystemTime::now(); - let quote = swap_provider.fetch_quote(&request).await?; + let quote = swap_provider.get_quote(&request).await?; let elapsed = SystemTime::now().duration_since(now).unwrap(); println!("<== elapsed: {:?}", elapsed); println!("<== quote: {:?}", quote); assert!(quote.to_value.parse::().unwrap() > 0); - let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::EstimateGas).await?; + let quote_data = swap_provider.get_quote_data("e, FetchQuoteData::EstimateGas).await?; println!("<== quote_data: {:?}", quote_data); Ok(()) diff --git a/gemstone/src/gem_swapper/mod.rs b/gemstone/src/gem_swapper/mod.rs index 6cefe352b..f0b16ebac 100644 --- a/gemstone/src/gem_swapper/mod.rs +++ b/gemstone/src/gem_swapper/mod.rs @@ -46,11 +46,11 @@ impl GemSwapper { } pub async fn fetch_quote_by_provider(&self, provider: SwapperProvider, request: SwapperQuoteRequest) -> Result { - self.inner.fetch_quote_by_provider(provider, request).await + self.inner.get_quote_by_provider(provider, request).await } pub async fn fetch_permit2_for_quote(&self, quote: &SwapperQuote) -> Result, SwapperError> { - self.inner.fetch_permit2_for_quote(quote).await + self.inner.get_permit2_for_quote(quote).await } pub async fn get_quote_data(&self, quote: &SwapperQuote, data: FetchQuoteData) -> Result { From 2826cc61f57dc5943563924e77a5f6591ecf4991 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:16:55 +0900 Subject: [PATCH 15/20] rename variables --- crates/swapper/src/relay/provider.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs index 11dbedd53..37bb465ad 100644 --- a/crates/swapper/src/relay/provider.rs +++ b/crates/swapper/src/relay/provider.rs @@ -89,10 +89,10 @@ where max_route_length: 6, }; - let quote_response = self.client.get_quote(relay_request).await?; + let response = self.client.get_quote(relay_request).await?; - let to_value = quote_response.details.currency_out.amount.clone(); - let eta_in_seconds = quote_response.details.time_estimate_u32(); + let to_value = response.details.currency_out.amount.clone(); + let eta_in_seconds = response.details.time_estimate_u32(); let quote = Quote { from_value, @@ -102,10 +102,10 @@ where routes: vec![Route { input: from_asset_id, output: to_asset_id, - route_data: serde_json::to_string("e_response).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?, + route_data: serde_json::to_string(&response).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?, gas_limit: None, }], - slippage_bps: quote_response.details.slippage_bps().unwrap_or(request.options.slippage.bps), + slippage_bps: response.details.slippage_bps().unwrap_or(request.options.slippage.bps), }, request: request.clone(), eta_in_seconds, @@ -116,11 +116,11 @@ where async fn get_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let route = quote.data.routes.first().ok_or(SwapperError::InvalidRoute)?; - let quote_response: RelayQuoteResponse = serde_json::from_str(&route.route_data).map_err(|_| SwapperError::InvalidRoute)?; + let response: RelayQuoteResponse = serde_json::from_str(&route.route_data).map_err(|_| SwapperError::InvalidRoute)?; let from_asset_id = quote.request.from_asset.asset_id(); - let approval = self.check_evm_approval(quote, "e_response, &from_asset_id).await?; - mapper::map_quote_data("e_response, approval) + let approval = self.check_evm_approval(quote, &response, &from_asset_id).await?; + mapper::map_quote_data(&response, approval) } async fn get_swap_result(&self, _chain: Chain, transaction_hash: &str) -> Result { From 27f32b08cebeaf0d1177b2633f865f80d0dfb1c2 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:35:32 +0900 Subject: [PATCH 16/20] cleanup tests --- crates/swapper/src/relay/mapper.rs | 28 +- .../request_base_usdc_to_eth_usdc.json | 276 --------------- .../request_base_usdc_to_sol_usdt.json | 322 ------------------ .../testdata/request_bsc_usdt_to_sol.json | 38 +++ .../relay/testdata/request_eth_to_btc.json | 42 +++ 5 files changed, 81 insertions(+), 625 deletions(-) delete mode 100644 crates/swapper/src/relay/testdata/request_base_usdc_to_eth_usdc.json delete mode 100644 crates/swapper/src/relay/testdata/request_base_usdc_to_sol_usdt.json create mode 100644 crates/swapper/src/relay/testdata/request_bsc_usdt_to_sol.json create mode 100644 crates/swapper/src/relay/testdata/request_eth_to_btc.json diff --git a/crates/swapper/src/relay/mapper.rs b/crates/swapper/src/relay/mapper.rs index 9a5422ae7..925b1c5dd 100644 --- a/crates/swapper/src/relay/mapper.rs +++ b/crates/swapper/src/relay/mapper.rs @@ -44,9 +44,7 @@ pub fn map_swap_result(request: &RelayRequest) -> SwapResult { #[cfg(test)] mod tests { use super::*; - use crate::relay::model::{ - CurrencyAmount, QuoteDetails, RelayCurrencyDetail, RelayQuoteResponse, RelayRequest, RelayRequestMetadata, RelayRequestsResponse, RelayStatus, Step, - }; + use crate::relay::model::{CurrencyAmount, QuoteDetails, RelayCurrencyDetail, RelayQuoteResponse, RelayRequest, RelayRequestMetadata, RelayStatus, Step}; use primitives::{AssetId, Chain, swap::SwapStatus}; #[test] @@ -140,28 +138,4 @@ mod tests { assert!(map_quote_data("e_response, None).is_err()); } - - #[test] - fn test_map_swap_result_base_usdc_to_eth_usdc() { - let response: RelayRequestsResponse = serde_json::from_str(include_str!("testdata/request_base_usdc_to_eth_usdc.json")).unwrap(); - let request = response.requests.first().unwrap(); - let result = map_swap_result(request); - - assert_eq!(result.status, SwapStatus::Completed); - let metadata = result.metadata.unwrap(); - assert_eq!(metadata.from_asset, AssetId::from_token(Chain::Base, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")); - assert_eq!(metadata.from_value, "11911543"); - assert_eq!(metadata.to_asset, AssetId::from_token(Chain::Ethereum, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")); - assert_eq!(metadata.to_value, "11707349"); - } - - #[test] - fn test_map_swap_result_base_usdc_to_sol_usdt_without_metadata() { - let response: RelayRequestsResponse = serde_json::from_str(include_str!("testdata/request_base_usdc_to_sol_usdt.json")).unwrap(); - let request = response.requests.first().unwrap(); - let result = map_swap_result(request); - - assert_eq!(result.status, SwapStatus::Completed); - assert!(result.metadata.is_none()); - } } diff --git a/crates/swapper/src/relay/testdata/request_base_usdc_to_eth_usdc.json b/crates/swapper/src/relay/testdata/request_base_usdc_to_eth_usdc.json deleted file mode 100644 index e62ff0ce2..000000000 --- a/crates/swapper/src/relay/testdata/request_base_usdc_to_eth_usdc.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "requests": [ - { - "id": "0x8f56957067e24868a69c22d858f841a9266c83c1045c16e5bb8c1da7f6061703", - "status": "success", - "user": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", - "recipient": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", - "data": { - "slippageTolerance": "200", - "failReason": "N/A", - "refundFailReason": "N/A", - "subsidizedRequest": false, - "fees": { - "gas": "96924", - "fixed": "20002", - "price": "936", - "gateway": "0" - }, - "feesUsd": { - "gas": "0.096921", - "fixed": "0.020001", - "price": "0.000935", - "gateway": "0.000000" - }, - "inTxs": [ - { - "fee": "847893217239", - "data": { - "to": "0x4cd00e387622c35bddb9b4c962c136462338bc31", - "data": "0xe8017952000000000000000000000000514bcb1f9aabb904e6106bd1052b66d2706dbbb7000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000b5c17701aad3b5e755a3fe465bd1d314a091638e2316e6f0a9caf29783d57f67c8b7b8", - "from": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", - "value": "0" - }, - "stateChanges": [ - { - "change": { - "data": { - "tokenKind": "ft", - "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" - }, - "kind": "token", - "balanceDiff": "-11911543" - }, - "address": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7" - }, - { - "change": { - "data": { - "tokenKind": "ft", - "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" - }, - "kind": "token", - "balanceDiff": "11911543" - }, - "address": "0x4cd00e387622c35bddb9b4c962c136462338bc31" - } - ], - "hash": "0xba65690811a4dd3efb444c1eacaa7a6255436b1e8c58ae56bdb110b3ccc39084", - "block": 42954356, - "type": "onchain", - "chainId": 8453, - "timestamp": 1772698059, - "status": "success" - } - ], - "currency": "usdc", - "currencyObject": {}, - "feeCurrency": "usdc", - "feeCurrencyObject": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "appFeeCurrencyObject": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "appFees": [ - { - "recipient": "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC", - "bps": "25", - "amount": "29778", - "amountUsd": "0.029777", - "amountUsdCurrent": "0.029777" - } - ], - "paidAppFees": [ - { - "recipient": "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC", - "bps": "25", - "amount": "29778", - "amountUsd": "0.029777", - "amountUsdCurrent": "0.029777" - } - ], - "metadata": { - "sender": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", - "recipient": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", - "currencyIn": { - "currency": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "amount": "11911543", - "amountFormatted": "11.911543", - "amountUsd": "11.909899", - "amountUsdCurrent": "11.910959", - "minimumAmount": "11911543" - }, - "currencyOut": { - "currency": { - "chainId": 1, - "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "amount": "11707349", - "amountFormatted": "11.707349", - "amountUsd": "11.705932", - "amountUsdCurrent": "11.706775", - "minimumAmount": "11473202" - }, - "rate": "0.9828574685916006", - "route": { - "origin": { - "inputCurrency": { - "currency": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "amount": "11911543", - "amountFormatted": "11.911543", - "amountUsd": "11.909899", - "minimumAmount": "11911543" - }, - "outputCurrency": { - "currency": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "amount": "11911543", - "amountFormatted": "11.911543", - "amountUsd": "11.909899", - "minimumAmount": "11911543" - }, - "router": "relay" - }, - "destination": { - "inputCurrency": { - "currency": { - "chainId": 1, - "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "amount": "11707349", - "amountFormatted": "11.707349", - "amountUsd": "11.705733", - "minimumAmount": "11473202" - }, - "outputCurrency": { - "currency": { - "chainId": 1, - "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "amount": "11707349", - "amountFormatted": "11.707349", - "amountUsd": "11.705733", - "minimumAmount": "11473202" - }, - "router": "relay" - } - } - }, - "price": "11707349", - "usesExternalLiquidity": false, - "timeEstimate": 7, - "outTxs": [ - { - "fee": "5764046694132", - "data": { - "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "data": "0x23b872dd000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef000000000000000000000000514bcb1f9aabb904e6106bd1052b66d2706dbbb70000000000000000000000000000000000000000000000000000000000b2a3d501aad3b5e755a3fe465bd1d314a091638e2316e6f0a9caf29783d57f67c8b7b8", - "from": "0xada5bb90d0de0bd1b6f3938708f49295a8d1f7cb", - "value": "0" - }, - "stateChanges": [ - { - "change": { - "data": { - "tokenKind": "ft", - "tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - }, - "kind": "token", - "balanceDiff": "-11707349" - }, - "address": "0xf70da97812cb96acdf810712aa562db8dfa3dbef" - }, - { - "change": { - "data": { - "tokenKind": "ft", - "tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - }, - "kind": "token", - "balanceDiff": "11707349" - }, - "address": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7" - } - ], - "hash": "0x3fee130ba0a8a4c1b2b75f16f6052f7cc749fb4d89b9777c97c35ed514af3399", - "block": 24589972, - "type": "onchain", - "chainId": 1, - "timestamp": 1772698103, - "status": "success" - } - ] - }, - "referrer": "gemwallet", - "createdAt": "2026-03-05T08:07:38.367Z", - "updatedAt": "2026-03-05T08:10:31.371Z" - } - ] -} diff --git a/crates/swapper/src/relay/testdata/request_base_usdc_to_sol_usdt.json b/crates/swapper/src/relay/testdata/request_base_usdc_to_sol_usdt.json deleted file mode 100644 index 98e2d38c1..000000000 --- a/crates/swapper/src/relay/testdata/request_base_usdc_to_sol_usdt.json +++ /dev/null @@ -1,322 +0,0 @@ -{ - "requests": [ - { - "id": "0x394b33c90fa8c29e1ce0c7bd1b728d28753ecb7c272ca50f6b195734d48dfa5b", - "status": "success", - "user": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", - "recipient": "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC", - "data": { - "slippageTolerance": "274", - "failReason": "N/A", - "refundFailReason": "N/A", - "subsidizedRequest": false, - "fees": { - "gas": "3657", - "fixed": "20000", - "price": "873", - "gateway": "7174" - }, - "feesUsd": { - "gas": "0.003656", - "fixed": "0.019999", - "price": "0.000872", - "gateway": "0.007173" - }, - "inTxs": [ - { - "fee": "764163694615", - "data": { - "to": "0x4cd00e387622c35bddb9b4c962c136462338bc31", - "data": "0xe8017952000000000000000000000000514bcb1f9aabb904e6106bd1052b66d2706dbbb7000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000002dc6c046a15fef562c289f6203afe32324e57129afadff4b86f5e40dcca0b4f5d4c652", - "from": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", - "value": "0" - }, - "stateChanges": [ - { - "change": { - "data": { - "tokenKind": "ft", - "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" - }, - "kind": "token", - "balanceDiff": "-3000000" - }, - "address": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7" - }, - { - "change": { - "data": { - "tokenKind": "ft", - "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" - }, - "kind": "token", - "balanceDiff": "3000000" - }, - "address": "0x4cd00e387622c35bddb9b4c962c136462338bc31" - } - ], - "hash": "0x9d99b1a71ac0b3d4b5eb4ea59a9991e954620839692a2aa99fb24490030d351a", - "block": 42747491, - "type": "onchain", - "chainId": 8453, - "timestamp": 1772284329, - "status": "success" - } - ], - "currency": "usdt", - "currencyObject": {}, - "feeCurrency": "usdc", - "feeCurrencyObject": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "appFeeCurrencyObject": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "appFees": [ - { - "recipient": "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC", - "bps": "25", - "amount": "7500", - "amountUsd": "0.007499", - "amountUsdCurrent": "0.007499" - } - ], - "paidAppFees": [ - { - "recipient": "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC", - "bps": "25", - "amount": "7500", - "amountUsd": "0.007499", - "amountUsdCurrent": "0.007499" - } - ], - "metadata": { - "sender": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", - "recipient": "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC", - "currencyIn": { - "currency": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "amount": "3000000", - "amountFormatted": "3.0", - "amountUsd": "2.999994", - "amountUsdCurrent": "2.999853", - "minimumAmount": "3000000" - }, - "currencyOut": { - "currency": { - "chainId": 792703809, - "address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", - "symbol": "USDT", - "name": "USDT", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/325/large/Tether.png?1696501661", - "verified": true - } - }, - "amount": "2960498", - "amountFormatted": "2.960498", - "amountUsd": "2.960492", - "amountUsdCurrent": "2.960353", - "minimumAmount": "2879355" - }, - "rate": "0.9868326666666666", - "route": { - "origin": { - "inputCurrency": { - "currency": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "amount": "3000000", - "amountFormatted": "3.0", - "amountUsd": "2.999994", - "minimumAmount": "3000000" - }, - "outputCurrency": { - "currency": { - "chainId": 8453, - "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - "symbol": "USDC", - "name": "USD Coin", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "verified": true - } - }, - "amount": "3000000", - "amountFormatted": "3.0", - "amountUsd": "2.999994", - "minimumAmount": "3000000" - }, - "router": "relay" - }, - "destination": { - "inputCurrency": { - "currency": { - "chainId": 792703809, - "address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", - "symbol": "USDT", - "name": "USDT", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/325/large/Tether.png?1696501661", - "verified": true - } - }, - "amount": "2960472", - "amountFormatted": "2.960472", - "amountUsd": "2.960466", - "minimumAmount": "2879355" - }, - "outputCurrency": { - "currency": { - "chainId": 792703809, - "address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", - "symbol": "USDT", - "name": "USDT", - "decimals": 6, - "metadata": { - "logoURI": "https://coin-images.coingecko.com/coins/images/325/large/Tether.png?1696501661", - "verified": true - } - }, - "amount": "2960472", - "amountFormatted": "2.960472", - "amountUsd": "2.960466", - "minimumAmount": "2879355" - }, - "router": "relay" - } - } - }, - "price": "2960498", - "usesExternalLiquidity": false, - "timeEstimate": 1, - "outTxs": [ - { - "fee": "142612", - "data": { - "signer": "F7p3dFrjRTbtRp8FRF6qHLomXbKRBzpvBLjtQcfcgmNe", - "instructions": [ - { - "data": "GAxM3u", - "keys": [], - "programId": "ComputeBudget111111111111111111111111111111" - }, - { - "data": "3HnbFZFth191", - "keys": [], - "programId": "ComputeBudget111111111111111111111111111111" - }, - { - "data": "3YZBNpy9smUb", - "keys": [ - { - "pubkey": "A2sPEp16HY5pqxrSeyb78wevY1UQtuoBAGXmG3TNmaU9", - "isSigner": false, - "isWritable": true - }, - { - "pubkey": "Bbkfm7UsDMY194BiWcT64MQ46Afbbe7zitL5taGeLRuB", - "isSigner": false, - "isWritable": true - }, - { - "pubkey": "F7p3dFrjRTbtRp8FRF6qHLomXbKRBzpvBLjtQcfcgmNe", - "isSigner": true, - "isWritable": true - } - ], - "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "data": "KsyQypGSzaMRjJXpPsegPR7Qx65p45wBRviG8MiEJDn3AQPwvPUcCJYwyvw4mM7UrfFCn5s7gWM29kdY25w3dzfSmw", - "keys": [], - "programId": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" - } - ] - }, - "stateChanges": [ - { - "change": { - "data": { - "tokenKind": "ft", - "tokenAddress": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" - }, - "kind": "token", - "balanceDiff": "-2960498" - }, - "address": "F7p3dFrjRTbtRp8FRF6qHLomXbKRBzpvBLjtQcfcgmNe" - }, - { - "change": { - "data": { - "tokenKind": "ft", - "tokenAddress": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" - }, - "kind": "token", - "balanceDiff": "2960498" - }, - "address": "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC" - }, - { - "change": { - "data": { - "tokenKind": "ft", - "tokenAddress": "11111111111111111111111111111111" - }, - "kind": "token", - "balanceDiff": "-142612" - }, - "address": "F7p3dFrjRTbtRp8FRF6qHLomXbKRBzpvBLjtQcfcgmNe" - } - ], - "hash": "3VMoJY5LiVU93J9jAQ23Py77rEp1z8xLUKRu2wcGfEvifuRhF4C1rL7y9PjjrGGQ7BknMDTXfiUv92wJAKe1XoSK", - "block": 403288806, - "type": "onchain", - "chainId": 792703809, - "timestamp": 1772284328, - "status": "success" - } - ] - }, - "referrer": "gemwallet", - "createdAt": "2026-02-28T13:12:07.863Z", - "updatedAt": "2026-02-28T13:12:18.372Z" - } - ] -} diff --git a/crates/swapper/src/relay/testdata/request_bsc_usdt_to_sol.json b/crates/swapper/src/relay/testdata/request_bsc_usdt_to_sol.json new file mode 100644 index 000000000..f80af330f --- /dev/null +++ b/crates/swapper/src/relay/testdata/request_bsc_usdt_to_sol.json @@ -0,0 +1,38 @@ +{ + "requests": [ + { + "id": "0xd2bcbf6c8e155411f6633067a29b1ee511d3d98b25db49393dd4ce58b00ee0f8", + "status": "success", + "data": { + "metadata": { + "sender": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", + "recipient": "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC", + "currencyIn": { + "currency": { + "chainId": 56, + "address": "0x55d398326f99059ff775485246999027b3197955", + "symbol": "USDT", + "name": "Tether USD", + "decimals": 18 + }, + "amount": "6000000000000000000", + "amountFormatted": "6.0", + "amountUsd": "6.000000" + }, + "currencyOut": { + "currency": { + "chainId": 792703809, + "address": "So11111111111111111111111111111111111111112", + "symbol": "WSOL", + "name": "Wrapped SOL", + "decimals": 9 + }, + "amount": "74432990", + "amountFormatted": "0.07443299", + "amountUsd": "5.806518" + } + } + } + } + ] +} diff --git a/crates/swapper/src/relay/testdata/request_eth_to_btc.json b/crates/swapper/src/relay/testdata/request_eth_to_btc.json new file mode 100644 index 000000000..3a20d33a8 --- /dev/null +++ b/crates/swapper/src/relay/testdata/request_eth_to_btc.json @@ -0,0 +1,42 @@ +{ + "requests": [ + { + "id": "0x66dfacf8279d88615a013041f7bb19e7a634b367e6d80d54c96b33339a55078f", + "status": "success", + "user": "0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7", + "recipient": "bc1qe7qlndxgfv76c0ulnfhh7j0vdwkqdkkl4yf9gm", + "data": { + "metadata": { + "sender": "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7", + "recipient": "bc1qe7qlndxgfv76c0ulnfhh7j0vdwkqdkkl4yf9gm", + "currencyIn": { + "currency": { + "chainId": 1, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "amount": "10000000000000000", + "amountFormatted": "0.01", + "amountUsd": "19.937560" + }, + "currencyOut": { + "currency": { + "chainId": 8253038, + "address": "bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8", + "symbol": "BTC", + "name": "Bitcoin", + "decimals": 8 + }, + "amount": "28619", + "amountFormatted": "0.00028619", + "amountUsd": "19.445550" + } + } + }, + "createdAt": "2026-03-03T06:28:25.979Z", + "updatedAt": "2026-03-03T07:11:36.416Z" + } + ] +} From bb9552fed4f0a0bff8936e161d46cc912d1d70ad Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:38:11 +0900 Subject: [PATCH 17/20] fetch permit -> get permit --- gemstone/src/gem_swapper/mod.rs | 2 +- gemstone/tests/ios/GemTest/GemTest/ViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gemstone/src/gem_swapper/mod.rs b/gemstone/src/gem_swapper/mod.rs index f0b16ebac..dd36a3c88 100644 --- a/gemstone/src/gem_swapper/mod.rs +++ b/gemstone/src/gem_swapper/mod.rs @@ -49,7 +49,7 @@ impl GemSwapper { self.inner.get_quote_by_provider(provider, request).await } - pub async fn fetch_permit2_for_quote(&self, quote: &SwapperQuote) -> Result, SwapperError> { + pub async fn get_permit2_for_quote(&self, quote: &SwapperQuote) -> Result, SwapperError> { self.inner.get_permit2_for_quote(quote).await } diff --git a/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift b/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift index bd21fe272..f07895988 100644 --- a/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift +++ b/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift @@ -39,7 +39,7 @@ public struct ViewModel: Sendable { public func fetchQuoteData(quote: SwapperQuote) async throws { let swapper = GemSwapper(rpcProvider: self.provider) - if let permit2 = try await swapper.fetchPermit2ForQuote(quote: quote) { + if let permit2 = try await swapper.getPermit2ForQuote(quote: quote) { print("<== permit2", permit2) } From a177853ab1a3478a44264c85e6d6c02b5ac522ae Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:46:39 +0900 Subject: [PATCH 18/20] Update mod.rs --- gemstone/src/gem_swapper/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gemstone/src/gem_swapper/mod.rs b/gemstone/src/gem_swapper/mod.rs index dd36a3c88..4988c353f 100644 --- a/gemstone/src/gem_swapper/mod.rs +++ b/gemstone/src/gem_swapper/mod.rs @@ -45,7 +45,7 @@ impl GemSwapper { self.inner.get_quote(request).await } - pub async fn fetch_quote_by_provider(&self, provider: SwapperProvider, request: SwapperQuoteRequest) -> Result { + pub async fn get_quote_by_provider(&self, provider: SwapperProvider, request: SwapperQuoteRequest) -> Result { self.inner.get_quote_by_provider(provider, request).await } From a7370695bbf203545b06f1207135ade8f0058ef4 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:41:54 +0900 Subject: [PATCH 19/20] Update ViewModel.swift --- gemstone/tests/ios/GemTest/GemTest/ViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift b/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift index f07895988..d5f894941 100644 --- a/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift +++ b/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift @@ -30,7 +30,7 @@ public struct ViewModel: Sendable { public func fetchQuoteById(_ request: SwapperQuoteRequest, provider: SwapProvider) async throws { let swapper = GemSwapper(rpcProvider: self.provider) - let quote = try await swapper.fetchQuoteByProvider(provider: provider, request: request) + let quote = try await swapper.getQuoteByProvider(provider: provider, request: request) self.dumpQuote(quote) try await self.fetchQuoteData(quote: quote) From 7ddc6238f08bedb461e5527374d4f1d1ecac004a Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:18:44 +0900 Subject: [PATCH 20/20] add STEP_TRANSACTION --- crates/swapper/src/relay/model.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/swapper/src/relay/model.rs b/crates/swapper/src/relay/model.rs index 1dca9a3f4..c05f13971 100644 --- a/crates/swapper/src/relay/model.rs +++ b/crates/swapper/src/relay/model.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; const STEP_SWAP: &str = "swap"; const STEP_DEPOSIT: &str = "deposit"; const STEP_APPROVE: &str = "approve"; +const STEP_TRANSACTION: &str = "transaction"; pub fn relay_trade_type(mode: &SwapMode) -> &'static str { match mode { @@ -65,7 +66,7 @@ impl RelayQuoteResponse { self.steps .iter() .find(|step| step.id == STEP_SWAP || step.id == STEP_DEPOSIT) - .or_else(|| self.steps.iter().find(|step| step.kind == "transaction" && step.id != STEP_APPROVE)) + .or_else(|| self.steps.iter().find(|step| step.kind == STEP_TRANSACTION && step.id != STEP_APPROVE)) .or_else(|| self.steps.iter().find(|step| step.step_data().is_some())) .and_then(Step::step_data) }