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/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/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/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 ed33a5dac..5679a7e3c 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, }; @@ -282,13 +282,13 @@ 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), }; - 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")?; @@ -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(); @@ -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 { .. })); } @@ -556,7 +556,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; @@ -588,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(()) @@ -617,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 bac921f1c..9c842a3dc 100644 --- a/crates/swapper/src/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -14,9 +14,8 @@ use crate::{ config::{DEFAULT_SWAP_FEE_BPS, get_swap_api_url}, cross_chain::VaultAddresses, models::{ApprovalType, SwapperChainAsset}, - relay, }; -use gem_client::{Client, ClientExt}; +use gem_client::Client; use primitives::{ Chain, ChainType, TransactionSwapMetadata, swap::{ApprovalData, ProxyQuote, ProxyQuoteRequest, SwapQuoteData}, @@ -172,15 +171,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] @@ -196,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(), @@ -228,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)?; @@ -268,14 +258,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 { @@ -291,16 +273,6 @@ 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 => { let base_url = get_swap_api_url("mayan/price"); let client = MayanPrice::new(base_url, self.rpc_provider.clone()); @@ -371,7 +343,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> { @@ -390,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); @@ -424,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); @@ -477,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(); @@ -507,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/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..5bbf37d39 100644 --- a/crates/swapper/src/relay/asset.rs +++ b/crates/swapper/src/relay/asset.rs @@ -1,8 +1,16 @@ +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 crate::{SwapperChainAsset, SwapperError, asset::*}; fn is_native_currency(chain: Chain, currency: &str) -> bool { match chain { @@ -24,3 +32,79 @@ 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_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), + ], + ), + 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_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![]), + SwapperChainAsset::Assets(Chain::Abstract, vec![]), + SwapperChainAsset::Assets(Chain::Celo, vec![]), + SwapperChainAsset::Assets(Chain::Stable, vec![]), + ] +}); + +pub fn asset_to_currency(asset_id: &AssetId) -> Result { + 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), + } +} + +#[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)).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)).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 0864da532..c92f0d999 100644 --- a/crates/swapper/src/relay/chain.rs +++ b/crates/swapper/src/relay/chain.rs @@ -1,31 +1,42 @@ 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 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)) - } + pub fn chain_id(&self) -> u64 { + match self { + Self::Evm(chain) => chain.chain_id(), } } - pub fn to_chain(&self) -> Chain { + pub fn from_chain(chain: &Chain) -> Option { + 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(), + Self::Evm(chain) => chain.to_chain(), } } + + pub fn from_chain_id(chain_id: u64) -> Option { + Some(Self::Evm(EVMChain::all().into_iter().find(|chain| chain.chain_id() == chain_id)?)) + } +} + +#[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!(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/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..925b1c5dd 100644 --- a/crates/swapper/src/relay/mapper.rs +++ b/crates/swapper/src/relay/mapper.rs @@ -1,7 +1,24 @@ -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_SWAP_GAS_LIMIT, + asset::map_currency_to_asset_id, + chain::RelayChain, + model::{RelayQuoteResponse, RelayRequest, StepData}, +}; +use crate::{SwapResult, SwapperError, SwapperProvider, SwapperQuoteData}; + +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 call_data = evm.data.clone().unwrap_or_default(); + Ok(SwapperQuoteData::new_contract(evm.to.clone(), evm.value.clone(), call_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 +44,54 @@ 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::{CurrencyAmount, QuoteDetails, RelayCurrencyDetail, RelayQuoteResponse, RelayRequest, RelayRequestMetadata, RelayStatus, Step}; use primitives::{AssetId, Chain, swap::SwapStatus}; + #[test] + fn test_map_evm_quote_data() { + 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("e_response, 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 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("e_response, Some(approval.clone())).unwrap(); + + assert_eq!(result.to, "0xrouter"); + assert_eq!(result.approval, Some(approval)); + assert_eq!(result.gas_limit, Some(DEFAULT_SWAP_GAS_LIMIT.to_string())); + } + #[test] fn test_map_swap_result_evm_to_evm() { let request = RelayRequest::mock( @@ -63,31 +125,17 @@ 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); + 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, + }; - 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"); + assert!(map_quote_data("e_response, None).is_err()); } } diff --git a/crates/swapper/src/relay/mod.rs b/crates/swapper/src/relay/mod.rs index 3b635ab79..6a0dbe712 100644 --- a/crates/swapper/src/relay/mod.rs +++ b/crates/swapper/src/relay/mod.rs @@ -1,8 +1,12 @@ 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; +const DEFAULT_SWAP_GAS_LIMIT: u64 = 150_000; + +pub use provider::Relay; diff --git a/crates/swapper/src/relay/model.rs b/crates/swapper/src/relay/model.rs index 030c88b98..c05f13971 100644 --- a/crates/swapper/src/relay/model.rs +++ b/crates/swapper/src/relay/model.rs @@ -1,6 +1,161 @@ +use std::collections::BTreeSet; + use gem_evm::address::ethereum_address_checksum; -use primitives::swap::SwapStatus; -use serde::Deserialize; +use primitives::swap::{SwapMode, SwapStatus}; +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 { + 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, + pub refund_to: String, + #[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 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 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 == STEP_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 { + 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() + } + + pub fn to_address(&self) -> Option { + Some(self.step_data()?.to_address()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StepItem { + pub data: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum StepData { + Evm(EvmStepData), +} + +impl StepData { + pub fn to_address(&self) -> String { + match self { + Self::Evm(evm) => evm.to.clone(), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EvmStepData { + pub to: String, + pub data: Option, + pub value: String, +} + +#[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")] @@ -77,16 +232,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 new file mode 100644 index 000000000..37bb465ad --- /dev/null +++ b/crates/swapper/src/relay/provider.rs @@ -0,0 +1,237 @@ +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::{ + 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, SwapperProvider, + SwapperQuoteData, approval::check_approval_erc20, config::get_swap_api_url, cross_chain::VaultAddresses, fees::resolve_max_quote_value, 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![]; + }; + vec![RelayAppFee { + recipient: fee.address.clone(), + fee: fee.bps.to_string(), + }] +} + +#[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 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)?; + + 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)?; + let destination_currency = asset_to_currency(&to_asset_id)?; + let app_fees = resolve_app_fees(request); + let from_value = resolve_max_quote_value(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: 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: request.wallet_address.clone(), + max_route_length: 6, + }; + + let response = self.client.get_quote(relay_request).await?; + + let to_value = response.details.currency_out.amount.clone(); + let eta_in_seconds = response.details.time_estimate_u32(); + + let quote = Quote { + from_value, + 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(&response).map_err(|e| SwapperError::ComputeQuoteError(e.to_string()))?, + gas_limit: None, + }], + slippage_bps: response.details.slippage_bps().unwrap_or(request.options.slippage.bps), + }, + request: request.clone(), + eta_in_seconds, + }; + + Ok(quote) + } + + async fn get_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { + let route = quote.data.routes.first().ok_or(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, &response, &from_asset_id).await?; + mapper::map_quote_data(&response, 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 { + let response = self.client.get_chains().await?; + Ok(VaultAddresses { + deposit: response.deposit_addresses(), + send: response.send_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 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)?; + + Ok(check_approval_erc20( + quote.request.wallet_address.clone(), + token, + spender, + 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.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()); + + 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.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()); + 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 4ffccc950..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}; +use super::model::{EvmStepData, RelayCurrency, RelayCurrencyDetail, RelayRequest, RelayRequestData, RelayRequestMetadata, RelayStatus, Step, StepData, StepItem}; impl RelayRequest { pub fn mock(status: RelayStatus, metadata: Option) -> Self { @@ -20,3 +20,27 @@ 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::Evm(EvmStepData { + to: to.to_string(), + data: Some(data.to_string()), + value: value.to_string(), + })), + }]), + } + } + + 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 a1f8f39fd..4cdd5d657 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}; @@ -132,7 +132,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()), ]; @@ -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..4988c353f 100644 --- a/gemstone/src/gem_swapper/mod.rs +++ b/gemstone/src/gem_swapper/mod.rs @@ -45,12 +45,12 @@ impl GemSwapper { self.inner.get_quote(request).await } - pub async fn fetch_quote_by_provider(&self, provider: SwapperProvider, request: SwapperQuoteRequest) -> Result { - self.inner.fetch_quote_by_provider(provider, request).await + pub async fn get_quote_by_provider(&self, provider: SwapperProvider, request: SwapperQuoteRequest) -> Result { + 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 + pub async fn get_permit2_for_quote(&self, quote: &SwapperQuote) -> Result, SwapperError> { + self.inner.get_permit2_for_quote(quote).await } pub async fn get_quote_data(&self, quote: &SwapperQuote, data: FetchQuoteData) -> Result { diff --git a/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift b/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift index bd21fe272..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) @@ -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) }