Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/swapper/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
}
Expand Down
1 change: 0 additions & 1 deletion crates/swapper/src/near_intents/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
32 changes: 2 additions & 30 deletions crates/swapper/src/proxy/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -172,15 +171,6 @@ impl ProxyProvider<RpcClient> {

Self::new_with_path(SwapperProvider::Mayan, "mayan", assets, rpc_provider)
}

pub fn new_relay(rpc_provider: Arc<dyn RpcProvider>) -> Self {
Self::new_with_path(
SwapperProvider::Relay,
"relay",
vec![SwapperChainAsset::All(Chain::Hyperliquid), SwapperChainAsset::All(Chain::Berachain)],
rpc_provider,
)
}
}

#[async_trait]
Expand Down Expand Up @@ -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 {
Expand All @@ -291,16 +273,6 @@ where

async fn get_vault_addresses(&self, _from_timestamp: Option<u64>) -> Result<VaultAddresses, 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)?;
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());
Expand Down Expand Up @@ -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> {
Expand Down
4 changes: 0 additions & 4 deletions crates/swapper/src/proxy/provider_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,3 @@ pub fn new_panora(rpc_provider: Arc<dyn RpcProvider>) -> ProxyProvider<RpcClient
pub fn new_mayan(rpc_provider: Arc<dyn RpcProvider>) -> ProxyProvider<RpcClient> {
ProxyProvider::new_mayan(rpc_provider)
}

pub fn new_relay(rpc_provider: Arc<dyn RpcProvider>) -> ProxyProvider<RpcClient> {
ProxyProvider::new_relay(rpc_provider)
}
69 changes: 67 additions & 2 deletions crates/swapper/src/relay/asset.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -24,3 +32,60 @@ pub fn map_currency_to_asset_id(chain: Chain, currency: &str) -> AssetId {
}
AssetId::from_token(chain, currency)
}

pub static SUPPORTED_CHAINS: LazyLock<Vec<SwapperChainAsset>> = 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<String, SwapperError> {
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)).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);
}
}
41 changes: 35 additions & 6 deletions crates/swapper/src/relay/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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<Self> {
match chain_id {
BITCOIN_CHAIN_ID => Some(Self::Bitcoin),
Expand All @@ -20,12 +44,17 @@ impl RelayChain {
}
}
}
}

pub fn to_chain(&self) -> Chain {
match self {
Self::Bitcoin => Chain::Bitcoin,
Self::Solana => Chain::Solana,
Self::Evm(evm_chain) => evm_chain.to_chain(),
}
#[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());
}
}
37 changes: 37 additions & 0 deletions crates/swapper/src/relay/client.rs
Original file line number Diff line number Diff line change
@@ -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<C>
where
C: Client + Clone + Send + Sync + Debug + 'static,
{
client: C,
}

impl<C> RelayClient<C>
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<RelayQuoteResponse, SwapperError> {
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<RelayRequestsResponse, SwapperError> {
let path = format!("/requests/v2?hash={}", transaction_hash);
self.client.get(&path).await.map_err(SwapperError::from)
}

pub async fn get_chains(&self) -> Result<RelayChainsResponse, SwapperError> {
self.client.get("/chains").await.map_err(SwapperError::from)
}
}
88 changes: 84 additions & 4 deletions crates/swapper/src/relay/mapper.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
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::{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 map_quote_data(steps: &[Step], approval: Option<ApprovalData>) -> Result<SwapperQuoteData, SwapperError> {
let step_data = get_step_data(steps)?;

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))
}
}
}

pub fn map_swap_result(request: &RelayRequest) -> SwapResult {
let metadata = request.data.as_ref().and_then(|d| d.metadata.as_ref()).and_then(|m| {
Expand All @@ -27,9 +53,38 @@ 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 result = map_quote_data(&steps, 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 approval = ApprovalData {
token: "0xtoken".to_string(),
spender: "0xrouter".to_string(),
value: "1000".to_string(),
};

let result = map_quote_data(&steps, 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(
Expand Down Expand Up @@ -90,4 +145,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.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());
}

#[test]
fn test_get_step_data_no_usable_steps() {
let steps = vec![Step::mock_empty("approve", "transaction")];
assert!(get_step_data(&steps).is_err());
}
}
8 changes: 6 additions & 2 deletions crates/swapper/src/relay/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading