diff --git a/Cargo.lock b/Cargo.lock index 621febca..a6228b62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3308,12 +3308,15 @@ dependencies = [ "gem_sui", "gem_ton", "hex", + "num-bigint 0.4.6", "primitives", "serde", "serde_json", + "serde_urlencoded", "thiserror", "tokio", "uniffi", + "url", ] [[package]] @@ -8160,16 +8163,6 @@ dependencies = [ "typed-store-error", ] -[[package]] -name = "swap_thorchain" -version = "1.0.0" -dependencies = [ - "async-trait", - "primitives", - "reqwest", - "serde", -] - [[package]] name = "syn" version = "0.15.44" diff --git a/Cargo.toml b/Cargo.toml index 5ec7f8c4..665171e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ members = [ "crates/security_*", "crates/gem_*", - "crates/swap_*", "crates/localizer", "crates/job_runner", @@ -53,6 +52,7 @@ default-members = [ typeshare = "1.0.3" serde = { version = "1.0.196", features = ["derive"] } serde_json = { version = "1.0.114" } +serde_urlencoded = { version = "0.7.1" } tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] } reqwest = { version = "0.12.3", features = ["json"] } reqwest-middleware = { version = "0.3.0" } diff --git a/crates/gem_evm/src/uniswap/deployment.rs b/crates/gem_evm/src/uniswap/deployment.rs index 6af853ff..5682e7d7 100644 --- a/crates/gem_evm/src/uniswap/deployment.rs +++ b/crates/gem_evm/src/uniswap/deployment.rs @@ -7,7 +7,7 @@ pub struct V3Deployment { pub universal_router: &'static str, } -pub fn get_deployment_by_chain(chain: Chain) -> Option { +pub fn get_deployment_by_chain(chain: &Chain) -> Option { // https://docs.uniswap.org/contracts/v3/reference/deployments/ match chain { Chain::Ethereum => Some(V3Deployment { diff --git a/crates/name_resolver/tests/integration_test.rs b/crates/name_resolver/tests/integration_test.rs index 2953a6e2..20089426 100644 --- a/crates/name_resolver/tests/integration_test.rs +++ b/crates/name_resolver/tests/integration_test.rs @@ -12,10 +12,7 @@ mod tests { block_on(async { let provider = Provider::new(String::from("https://eth.llamarpc.com")); let addres = provider.resolve_name("vitalik.eth", Chain::Ethereum).await; - assert_eq!( - addres.unwrap(), - "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_lowercase() - ) + assert_eq!(addres.unwrap(), "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".to_lowercase()) }); } @@ -24,10 +21,7 @@ mod tests { block_on(async { let client = BNSClient::new(String::from("https://resolver-api.basename.app")); let addres = client.resolve("hello.base", Chain::Base).await; - assert_eq!( - addres.unwrap(), - "0x4fb3f133951bF1B2d52fF6CEab2c703fbB6E98cC" - ) + assert_eq!(addres.unwrap(), "0x4fb3f133951bF1B2d52fF6CEab2c703fbB6E98cC") }); } } diff --git a/crates/primitives/src/big_number_formatter.rs b/crates/primitives/src/big_number_formatter.rs index a8ad1002..11b1d188 100644 --- a/crates/primitives/src/big_number_formatter.rs +++ b/crates/primitives/src/big_number_formatter.rs @@ -36,15 +36,8 @@ mod tests { assert_eq!(result, "0.4567"); // Test case 4: u256 input - let result = BigNumberFormatter::value( - "115792089237316195423570985008687907853269984665640564039457000000000000000000", - 18, - ) - .unwrap(); - assert_eq!( - result, - "115792089237316195423570985008687907853269984665640564039457" - ); + let result = BigNumberFormatter::value("115792089237316195423570985008687907853269984665640564039457000000000000000000", 18).unwrap(); + assert_eq!(result, "115792089237316195423570985008687907853269984665640564039457"); // Test case 5: Invalid input let result = BigNumberFormatter::value("abc", 2); diff --git a/crates/swap_thorchain/Cargo.toml b/crates/swap_thorchain/Cargo.toml deleted file mode 100644 index 913c9c09..00000000 --- a/crates/swap_thorchain/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "swap_thorchain" -edition = { workspace = true } -version = { workspace = true } - -[dependencies] -serde.workspace = true -reqwest.workspace = true -async-trait.workspace = true - -primitives = { path = "../primitives" } diff --git a/crates/swap_thorchain/src/client.rs b/crates/swap_thorchain/src/client.rs deleted file mode 100644 index 5cca0c17..00000000 --- a/crates/swap_thorchain/src/client.rs +++ /dev/null @@ -1,86 +0,0 @@ -use primitives::{AssetId, Chain}; - -#[allow(unused)] -pub struct ThorchainSwapClient { - api_url: String, - client: reqwest::Client, -} - -const NATIVE_ADDRESS_DOGE: &str = "DOGE.DOGE"; -const NATIVE_ADDRESS_RUNE: &str = "THOR.RUNE"; -const NATIVE_ADDRESS_COSMOS: &str = "GAIA.ATOM"; -const NATIVE_BITCOIN: &str = "BTC.BTC"; -const NATIVE_LITECOIN: &str = "LTC.LTC"; -const NATIVE_BSC_BNB: &str = "BSC.BNB"; - -impl ThorchainSwapClient { - pub fn new(api_url: String) -> Self { - let client = reqwest::Client::builder().build().unwrap(); - - Self { - client, - api_url, - } - } - - pub fn get_asset(&self, asset_id: AssetId) -> Option { - match asset_id.chain { - Chain::Thorchain => Some(NATIVE_ADDRESS_RUNE.into()), - Chain::Doge => Some(NATIVE_ADDRESS_DOGE.into()), - Chain::Cosmos => Some(NATIVE_ADDRESS_COSMOS.into()), - Chain::Bitcoin => Some(NATIVE_BITCOIN.into()), - Chain::Litecoin => Some(NATIVE_LITECOIN.into()), - Chain::SmartChain => Some(NATIVE_BSC_BNB.into()), - _ => None, - } - } - // - // pub async fn get_quote(&self, quote: SwapQuoteProtocolRequest) -> Result { - // let from_asset = self.get_asset(quote.from_asset.clone())?; - // let to_asset = self.get_asset(quote.to_asset.clone())?; - // - // let request = QuoteRequest { - // from_asset, - // to_asset, - // amount: quote.amount.clone(), - // destination: quote.destination_address.clone(), - // affiliate: self.fee_referral_address.clone(), - // affiliate_bps: (self.fee * 100.0) as i64, - // }; - // let quote_swap = self.get_swap_quote(request).await?; - // - // let data = if quote.include_data { - // let data = SwapQuoteData { - // to: quote_swap.inbound_address.unwrap_or_default(), - // value: quote.amount.clone(), - // data: quote_swap.memo, - // }; - // Some(data) - // } else { - // None - // }; - // - // let quote = SwapQuote { - // chain_type: quote.from_asset.clone().chain.chain_type(), - // from_amount: quote.amount.clone(), - // to_amount: quote_swap.expected_amount_out.to_string(), - // fee_percent: self.fee as f32, - // provider: PROVIDER_NAME.into(), - // data, - // approval: None, - // }; - // Ok(quote) - // } - // - // pub async fn get_swap_quote(&self, request: QuoteRequest) -> Result { - // let url = format!("{}/thorchain/quote/swap", self.api_url); - // Ok(self - // .client - // .get(&url) - // .query(&request) - // .send() - // .await? - // .json::() - // .await?) - // } -} diff --git a/crates/swap_thorchain/src/lib.rs b/crates/swap_thorchain/src/lib.rs deleted file mode 100644 index 55a0313c..00000000 --- a/crates/swap_thorchain/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod client; -pub mod model; diff --git a/crates/swap_thorchain/src/model.rs b/crates/swap_thorchain/src/model.rs deleted file mode 100644 index 935b37bd..00000000 --- a/crates/swap_thorchain/src/model.rs +++ /dev/null @@ -1,18 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QuoteRequest { - pub from_asset: String, - pub to_asset: String, - pub amount: String, - pub destination: String, - pub affiliate: String, - pub affiliate_bps: i64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QuoteResponse { - pub expected_amount_out: String, - pub inbound_address: Option, - pub memo: String, -} diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index 7d0ae853..a8bf6a93 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -29,10 +29,13 @@ anyhow.workspace = true base64.workspace = true serde.workspace = true serde_json.workspace = true +serde_urlencoded.workspace = true async-trait.workspace = true alloy-core.workspace = true alloy-primitives.workspace = true hex.workspace = true +url.workspace = true +num-bigint.workspace = true [build-dependencies] uniffi = { workspace = true, features = ["build"] } diff --git a/gemstone/src/config/node.rs b/gemstone/src/config/node.rs index 716c872f..47db2abe 100644 --- a/gemstone/src/config/node.rs +++ b/gemstone/src/config/node.rs @@ -29,24 +29,19 @@ impl Node { } pub fn get_nodes() -> HashMap> { - Chain::all() - .into_iter() - .map(|chain| (chain.to_string(), get_nodes_for_chain(chain))) - .collect() + Chain::all().into_iter().map(|chain| (chain.to_string(), get_nodes_for_chain(chain))).collect() } pub fn get_nodes_for_chain(chain: Chain) -> Vec { match chain { Chain::Bitcoin | Chain::Litecoin => vec![], Chain::Ethereum => vec![ - Node::new("https://eth.llamarpc.com", NodePriority::High), Node::new("https://ethereum.publicnode.com", NodePriority::High), Node::new("https://rpc.ankr.com/eth", NodePriority::High), Node::new("https://ethereum-rpc.polkachu.com", NodePriority::High), Node::new("https://eth.merkle.io", NodePriority::High), ], Chain::SmartChain => vec![ - Node::new("https://binance.llamarpc.com", NodePriority::High), Node::new("https://bsc.publicnode.com", NodePriority::High), Node::new("https://bsc.merkle.io", NodePriority::High), ], @@ -114,7 +109,7 @@ pub fn get_nodes_for_chain(chain: Chain) -> Vec { ], Chain::Celestia => vec![ Node::new("https://celestia-rest.publicnode.com", NodePriority::High), - Node::new("https://celestia-api.polkachu.com", NodePriority::High) + Node::new("https://celestia-api.polkachu.com", NodePriority::High), ], Chain::Injective => vec![ Node::new("https://injective-rest.publicnode.com", NodePriority::High), @@ -129,9 +124,7 @@ pub fn get_nodes_for_chain(chain: Chain) -> Vec { Node::new("https://pacific-rpc.manta.network/http", NodePriority::High), Node::new("https://manta-pacific.drpc.org", NodePriority::High), ], - Chain::Blast => vec![ - Node::new("https://blast-rpc.polkachu.com", NodePriority::High), - ], + Chain::Blast => vec![Node::new("https://blast-rpc.polkachu.com", NodePriority::High)], Chain::Noble => vec![ Node::new("https://rest.cosmos.directory/noble", NodePriority::High), Node::new("https://noble-api.polkachu.com", NodePriority::High), @@ -140,14 +133,10 @@ pub fn get_nodes_for_chain(chain: Chain) -> Vec { Node::new("https://zksync.drpc.org", NodePriority::High), Node::new("https://mainnet.era.zksync.io", NodePriority::High), ], - Chain::Linea => vec![ - Node::new("https://linea-rpc.polkachu.com", NodePriority::High), - ], + Chain::Linea => vec![Node::new("https://linea-rpc.polkachu.com", NodePriority::High)], Chain::Mantle => vec![Node::new("https://rpc.ankr.com/mantle", NodePriority::High)], Chain::Celo => vec![Node::new("https://rpc.ankr.com/celo", NodePriority::High)], Chain::Near => vec![Node::new("https://rpc.mainnet.near.org", NodePriority::High)], - Chain::World => vec![ - Node::new("https://worldchain-mainnet.gateway.tenderly.co", NodePriority::High) - ], + Chain::World => vec![Node::new("https://worldchain-mainnet.gateway.tenderly.co", NodePriority::High)], } } diff --git a/gemstone/src/config/swap_config.rs b/gemstone/src/config/swap_config.rs index 5cb82a5f..0b5f678b 100644 --- a/gemstone/src/config/swap_config.rs +++ b/gemstone/src/config/swap_config.rs @@ -1,29 +1,45 @@ #[derive(uniffi::Record, Debug, Clone, PartialEq)] pub struct SwapConfig { - slippage_bps: u32, - referral_fee: SwapReferralFees, + pub default_slippage_bps: u32, + pub referral_fee: SwapReferralFees, } -#[derive(uniffi::Record, Debug, Clone, PartialEq)] +#[derive(uniffi::Record, Default, Debug, Clone, PartialEq)] pub struct SwapReferralFees { - evm: SwapReferralFee, - solana: SwapReferralFee, - thorchain: SwapReferralFee, + pub evm: SwapReferralFee, + pub solana: SwapReferralFee, + pub thorchain: SwapReferralFee, } -#[derive(uniffi::Record, Debug, Clone, PartialEq)] +#[derive(uniffi::Record, Default, Debug, Clone, PartialEq)] pub struct SwapReferralFee { - address: String, - bps: u32, + pub address: String, + pub bps: u32, +} + +impl SwapReferralFees { + pub fn evm(evm: SwapReferralFee) -> SwapReferralFees { + SwapReferralFees { + evm, + solana: SwapReferralFee::default(), + thorchain: SwapReferralFee::default(), + } + } } pub fn get_swap_config() -> SwapConfig { SwapConfig { - slippage_bps: 100, + default_slippage_bps: 100, referral_fee: SwapReferralFees { - evm: SwapReferralFee { address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), bps: 50 }, - solana: SwapReferralFee { address: "5fmLrs2GuhfDP1B51ziV5Kd1xtAr9rw1jf3aQ4ihZ2gy".into(), bps: 50 }, - thorchain: SwapReferralFee { address: "gemwallet".into(), bps: 50 }, + evm: SwapReferralFee { + address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), + bps: 50, + }, + solana: SwapReferralFee { + address: "5fmLrs2GuhfDP1B51ziV5Kd1xtAr9rw1jf3aQ4ihZ2gy".into(), + bps: 50, + }, + thorchain: SwapReferralFee { address: "g1".into(), bps: 50 }, }, } -} \ No newline at end of file +} diff --git a/gemstone/src/swapper/mod.rs b/gemstone/src/swapper/mod.rs index 09f09d63..a37c9fd0 100644 --- a/gemstone/src/swapper/mod.rs +++ b/gemstone/src/swapper/mod.rs @@ -8,12 +8,16 @@ mod custom_types; mod models; mod permit2_data; mod slippage; +mod thorchain; mod uniswap; + use models::*; +use primitives::Chain; #[async_trait] pub trait GemSwapProvider: Send + Sync + Debug { fn name(&self) -> &'static str; + async fn supported_chains(&self) -> Result, SwapperError>; async fn fetch_quote(&self, request: &SwapQuoteRequest, provider: Arc) -> Result; async fn fetch_quote_data(&self, quote: &SwapQuote, provider: Arc, data: FetchQuoteData) -> Result; } @@ -30,7 +34,7 @@ impl GemSwapper { fn new(rpc_provider: Arc) -> Self { Self { rpc_provider, - swappers: vec![Box::new(uniswap::UniswapV3::new())], + swappers: vec![Box::new(uniswap::UniswapV3::new()), Box::new(thorchain::ThorChain::new())], } } diff --git a/gemstone/src/swapper/models.rs b/gemstone/src/swapper/models.rs index 884bb9ac..c13e6e42 100644 --- a/gemstone/src/swapper/models.rs +++ b/gemstone/src/swapper/models.rs @@ -1,6 +1,8 @@ use primitives::{AssetId, ChainType}; use std::fmt::Debug; +use crate::config::swap_config::SwapReferralFees; + use super::permit2_data::Permit2Data; static DEFAULT_SLIPPAGE_BPS: u32 = 300; @@ -40,16 +42,10 @@ pub struct SwapQuoteRequest { pub options: Option, } -#[derive(Debug, Default, Clone, uniffi::Record)] -pub struct GemSwapFee { - pub bps: u32, - pub address: String, -} - #[derive(Debug, Clone, uniffi::Record)] pub struct GemSwapOptions { pub slippage_bps: u32, - pub fee: Option, + pub fee: Option, pub preferred_providers: Vec, } diff --git a/gemstone/src/swapper/permit2_data.rs b/gemstone/src/swapper/permit2_data.rs index 15c13b85..967c9c99 100644 --- a/gemstone/src/swapper/permit2_data.rs +++ b/gemstone/src/swapper/permit2_data.rs @@ -81,7 +81,7 @@ where #[uniffi::export] pub fn permit2_data_to_eip712_json(chain: Chain, data: PermitSingle) -> Result { let chain_id = chain.network_id(); - let contract = get_deployment_by_chain(chain).ok_or(SwapperError::NotImplemented)?.permit2; + let contract = get_deployment_by_chain(&chain).ok_or(SwapperError::NotImplemented)?.permit2; let message = Permit2Message { domain: EIP712Domain { name: "Permit2".to_string(), diff --git a/gemstone/src/swapper/thorchain/client.rs b/gemstone/src/swapper/thorchain/client.rs new file mode 100644 index 00000000..3046d0e7 --- /dev/null +++ b/gemstone/src/swapper/thorchain/client.rs @@ -0,0 +1,90 @@ +use crate::network::{AlienHttpMethod, AlienProvider, AlienTarget}; +use crate::swapper::models::SwapperError; +use crate::swapper::thorchain::model::{QuoteSwapRequest, QuoteSwapResponse}; +use primitives::AssetId; +use std::sync::Arc; + +use super::model::ThorChainAsset; + +#[derive(Debug)] +pub struct ThorChainSwapClient { + provider: Arc, +} + +impl ThorChainSwapClient { + pub fn new(provider: Arc) -> Self { + Self { provider } + } + + pub async fn get_quote( + &self, + endpoint: &str, + from_asset: AssetId, + to_asset: AssetId, + value: String, + affiliate: String, + affiliate_bps: i64, + ) -> Result { + let from_asset = ThorChainAsset::from_chain(&from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; + let to_asset = ThorChainAsset::from_chain(&to_asset.chain).ok_or(SwapperError::NotSupportedChain)?; + let params = QuoteSwapRequest { + from_asset: from_asset.short_name().to_string(), + to_asset: to_asset.short_name().to_string(), + amount: value, + affiliate, + affiliate_bps, + }; + let query = serde_urlencoded::to_string(params).unwrap(); + let url = format!("{}{}?{}", endpoint, "/thorchain/quote/swap", query); + + let target = AlienTarget { + url, + method: AlienHttpMethod::Get, + headers: None, + body: None, + }; + + let data = self + .provider + .request(target) + .await + .map_err(|err| SwapperError::NetworkError { msg: err.to_string() })?; + + let result: QuoteSwapResponse = serde_json::from_slice(&data).map_err(|err| SwapperError::NetworkError { msg: err.to_string() })?; + + Ok(result) + } + + // https://dev.thorchain.org/concepts/memos.html#swap + pub fn get_memo(to_asset: AssetId, destination_address: String, fee_address: String, bps: u32) -> Option { + let chain = ThorChainAsset::from_chain(&to_asset.clone().chain)?; + Some(format!("=:{}:{}::{}:{}", chain.short_name(), destination_address, fee_address, bps)) + } +} + +#[cfg(test)] +mod tests { + use primitives::Chain; + + use super::*; + + #[tokio::test] + async fn test_get_memo() { + let destination_address = "0x1234567890abcdef".to_string(); + let fee_address = "0xabcdef1234567890".to_string(); + let bps = 50; + + assert_eq!( + ThorChainSwapClient::get_memo(Chain::SmartChain.as_asset_id(), destination_address.clone(), fee_address.clone(), bps), + Some("=:s:0x1234567890abcdef::0xabcdef1234567890:50".into()) + ); + assert_eq!( + ThorChainSwapClient::get_memo(Chain::Ethereum.as_asset_id(), destination_address.clone(), fee_address.clone(), bps), + Some("=:e:0x1234567890abcdef::0xabcdef1234567890:50".into()) + ); + assert_eq!( + ThorChainSwapClient::get_memo(Chain::Doge.as_asset_id(), destination_address.clone(), fee_address.clone(), bps), + Some("=:d:0x1234567890abcdef::0xabcdef1234567890:50".into()) + ); + } +} diff --git a/gemstone/src/swapper/thorchain/mod.rs b/gemstone/src/swapper/thorchain/mod.rs new file mode 100644 index 00000000..77eb3167 --- /dev/null +++ b/gemstone/src/swapper/thorchain/mod.rs @@ -0,0 +1,170 @@ +mod client; +mod model; + +use model::ThorChainAsset; +use num_bigint::BigInt; +use std::str::FromStr; + +use crate::network::AlienProvider; +use crate::swapper::models::{ApprovalType, FetchQuoteData, SwapProviderData, SwapQuote, SwapQuoteData, SwapQuoteRequest, SwapperError}; +use crate::swapper::thorchain::client::ThorChainSwapClient; +use crate::swapper::GemSwapProvider; +use async_trait::async_trait; +use primitives::{Asset, Chain, ChainType}; +use std::sync::Arc; + +use super::SwapRoute; + +#[derive(Debug)] +pub struct ThorChain {} + +impl ThorChain { + pub fn new() -> Self { + Self {} + } + + fn data(&self, chain: Chain, memo: String) -> String { + match chain { + Chain::Thorchain | Chain::Litecoin | Chain::Doge | Chain::Bitcoin => memo, + _ => hex::encode(memo.as_bytes()), + } + } + + fn value_from(&self, value: String, decimals: i32) -> BigInt { + let decimals = decimals - 8; + if decimals > 0 { + BigInt::from_str(value.as_str()).unwrap() / BigInt::from(10).pow(decimals as u32) + } else { + BigInt::from_str(value.as_str()).unwrap() * BigInt::from(10).pow(decimals.unsigned_abs()) + } + } + + fn value_to(&self, value: String, decimals: i32) -> BigInt { + let decimals = decimals - 8; + if decimals > 0 { + BigInt::from_str(value.as_str()).unwrap() * BigInt::from(10).pow((decimals).unsigned_abs()) + } else { + BigInt::from_str(value.as_str()).unwrap() / BigInt::from(10).pow((decimals).unsigned_abs()) + } + } +} + +#[async_trait] +impl GemSwapProvider for ThorChain { + fn name(&self) -> &'static str { + "THORChain" + } + + async fn supported_chains(&self) -> Result, SwapperError> { + let chains: Vec = Chain::all() + .into_iter() + .filter_map(|chain| ThorChainAsset::from_chain(&chain).map(|name| name.chain())) + .collect(); + Ok(chains) + } + + async fn fetch_quote(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { + let endpoint = provider + .get_endpoint(Chain::Thorchain) + .map_err(|err| SwapperError::NetworkError { msg: err.to_string() })?; + let client = ThorChainSwapClient::new(provider); + + let from_decimals = Asset::from_chain(request.clone().from_asset.chain).decimals; + let to_decimals = Asset::from_chain(request.clone().to_asset.chain).decimals; + + let value = self.value_from(request.clone().value, from_decimals); + let fee = request.options.clone().unwrap_or_default().fee.unwrap_or_default().thorchain; + + let quote = client + .get_quote( + endpoint.as_str(), + request.clone().from_asset, + request.to_asset.clone(), + value.to_string(), + fee.address, + fee.bps.into(), + ) + .await?; + + let to_value = self.value_to(quote.expected_amount_out, to_decimals); + + let quote = SwapQuote { + chain_type: ChainType::Ethereum, + from_value: request.clone().value, + to_value: to_value.to_string(), + provider: SwapProviderData { + name: self.name().to_string(), + routes: vec![SwapRoute { + route_type: quote.inbound_address.unwrap_or_default(), + input: request.clone().from_asset.to_string(), + output: request.clone().to_asset.to_string(), + fee_tier: "".to_string(), + gas_estimate: None, + }], + }, + approval: ApprovalType::None, + request: request.clone(), + }; + + Ok(quote) + } + + async fn fetch_quote_data(&self, quote: &SwapQuote, _provider: Arc, _data: FetchQuoteData) -> Result { + let fee = quote.request.options.clone().unwrap_or_default().fee.unwrap_or_default().thorchain; + let memo = ThorChainSwapClient::get_memo(quote.request.to_asset.clone(), quote.request.destination_address.clone(), fee.address, fee.bps).unwrap(); + + let to = quote.provider.routes.first().unwrap().route_type.clone(); + let data: String = self.data(quote.request.from_asset.clone().chain, memo); + + let data = SwapQuoteData { + to, + value: quote.request.value.clone(), + data, + }; + + Ok(data) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_value_from() { + let thorchain = ThorChain::new(); + + let value = "1000000000".to_string(); + + let result = thorchain.value_from(value.clone(), 18); + assert_eq!(result, BigInt::from_str("0").unwrap()); + + let result = thorchain.value_from(value.clone(), 10); + assert_eq!(result, BigInt::from_str("10000000").unwrap()); + + let result = thorchain.value_from(value.clone(), 6); + assert_eq!(result, BigInt::from_str("100000000000").unwrap()); + + let result = thorchain.value_from(value.clone(), 8); + assert_eq!(result, BigInt::from(1000000000)); + } + + #[test] + fn test_value_to() { + let thorchain = ThorChain::new(); + + let value = "10000000".to_string(); + + let result = thorchain.value_to(value.clone(), 18); + assert_eq!(result, BigInt::from_str("100000000000000000").unwrap()); + + let result = thorchain.value_to(value.clone(), 10); + assert_eq!(result, BigInt::from(1000000000)); + + let result = thorchain.value_to(value.clone(), 6); + assert_eq!(result, BigInt::from(100000)); + + let result = thorchain.value_to(value.clone(), 8); + assert_eq!(result, BigInt::from(10000000)); + } +} diff --git a/gemstone/src/swapper/thorchain/model.rs b/gemstone/src/swapper/thorchain/model.rs new file mode 100644 index 00000000..06f5d9f2 --- /dev/null +++ b/gemstone/src/swapper/thorchain/model.rs @@ -0,0 +1,75 @@ +use primitives::Chain; +use serde::{Deserialize, Serialize}; + +pub enum ThorChainAsset { + Doge, + Thorchain, + Ethereum, + Cosmos, + Bitcoin, + Litecoin, + SmartChain, + AvalancheC, +} + +// https://dev.thorchain.org/concepts/memo-length-reduction.html +impl ThorChainAsset { + pub fn short_name(&self) -> &str { + match self { + ThorChainAsset::Doge => "d", // DOGE.DOGE + ThorChainAsset::Thorchain => "r", // THOR.RUNE + ThorChainAsset::Ethereum => "e", // "ETH.ETH" + ThorChainAsset::Cosmos => "g", // GAIA.ATOM + ThorChainAsset::Bitcoin => "b", // BTC.BTC + ThorChainAsset::Litecoin => "l", // LTC.LTC + ThorChainAsset::SmartChain => "s", // BSC.BNB + ThorChainAsset::AvalancheC => "a", // AVAX.AVAX + } + } + + pub fn chain(&self) -> Chain { + match self { + ThorChainAsset::Doge => Chain::Doge, + ThorChainAsset::Thorchain => Chain::Thorchain, + ThorChainAsset::Ethereum => Chain::Ethereum, + ThorChainAsset::Cosmos => Chain::Cosmos, + ThorChainAsset::Bitcoin => Chain::Bitcoin, + ThorChainAsset::Litecoin => Chain::Litecoin, + ThorChainAsset::SmartChain => Chain::SmartChain, + ThorChainAsset::AvalancheC => Chain::AvalancheC, + } + } + + pub fn from_chain(chain: &Chain) -> Option { + match chain { + Chain::Thorchain => Some(ThorChainAsset::Thorchain), + Chain::Doge => Some(ThorChainAsset::Doge), + Chain::Cosmos => Some(ThorChainAsset::Cosmos), + Chain::Bitcoin => Some(ThorChainAsset::Bitcoin), + Chain::Litecoin => Some(ThorChainAsset::Litecoin), + Chain::SmartChain => Some(ThorChainAsset::SmartChain), + Chain::Ethereum => Some(ThorChainAsset::Ethereum), + Chain::AvalancheC => Some(ThorChainAsset::AvalancheC), + _ => None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QuoteSwapRequest { + pub from_asset: String, + pub to_asset: String, + pub amount: String, + pub affiliate: String, + pub affiliate_bps: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QuoteSwapResponse { + pub expected_amount_out: String, + pub inbound_address: Option, + pub fees: QuoteFees, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QuoteFees {} diff --git a/gemstone/src/swapper/uniswap/mod.rs b/gemstone/src/swapper/uniswap/mod.rs index bb9d0495..8d6f7455 100644 --- a/gemstone/src/swapper/uniswap/mod.rs +++ b/gemstone/src/swapper/uniswap/mod.rs @@ -62,7 +62,7 @@ impl UniswapV3 { Self {} } - pub fn support_chain(&self, chain: Chain) -> bool { + pub fn support_chain(&self, chain: &Chain) -> bool { get_deployment_by_chain(chain).is_some() } @@ -95,25 +95,21 @@ impl UniswapV3 { } fn build_quoter_request(request: &SwapQuoteRequest, quoter_v2: &str, amount_in: U256, path: &Bytes) -> EthereumRpc { - let calldata: Vec = match request.mode { - GemSwapMode::ExactIn => { - let input_call = IQuoterV2::quoteExactInputCall { - path: path.clone(), - amountIn: amount_in, - }; - input_call.abi_encode() + let call_data: Vec = match request.mode { + GemSwapMode::ExactIn => IQuoterV2::quoteExactInputCall { + path: path.clone(), + amountIn: amount_in, } - GemSwapMode::ExactOut => { - let output_call = IQuoterV2::quoteExactOutputCall { - path: path.clone(), - amountOut: amount_in, - }; - output_call.abi_encode() + .abi_encode(), + GemSwapMode::ExactOut => IQuoterV2::quoteExactOutputCall { + path: path.clone(), + amountOut: amount_in, } + .abi_encode(), }; EthereumRpc::Call( - TransactionObject::new_call_with_from(&request.wallet_address, quoter_v2, calldata), + TransactionObject::new_call_with_from(&request.wallet_address, quoter_v2, call_data), BlockParameter::Latest, ) } @@ -139,7 +135,7 @@ impl UniswapV3 { permit: Option, ) -> Result, SwapperError> { let options = request.options.clone().unwrap_or_default(); - let fee_options = options.fee.unwrap_or_default(); + let fee_options = options.fee.unwrap_or_default().evm; let recipient = Address::from_str(&request.wallet_address).map_err(|_| SwapperError::InvalidAddress { address: request.wallet_address.clone(), })?; @@ -225,7 +221,7 @@ impl UniswapV3 { wallet_address: Address, token: &str, amount: U256, - chain: Chain, + chain: &Chain, provider: Arc, ) -> Result { let deployment = get_deployment_by_chain(chain).ok_or(SwapperError::NotSupportedChain)?; @@ -290,7 +286,7 @@ impl UniswapV3 { &self, rpc_calls: &[EthereumRpc], provider: Arc, - chain: Chain, + chain: &Chain, ) -> Result>, SwapperError> { let requests: Vec = rpc_calls .iter() @@ -299,7 +295,7 @@ impl UniswapV3 { .collect(); let endpoint = provider - .get_endpoint(chain) + .get_endpoint(*chain) .map_err(|err| SwapperError::NetworkError { msg: err.to_string() })?; let targets = vec![batch_into_target(&requests, &endpoint)]; @@ -336,16 +332,20 @@ impl GemSwapProvider for UniswapV3 { UNISWAP } + async fn supported_chains(&self) -> Result, SwapperError> { + Ok(Chain::all().iter().filter(|x| self.support_chain(x)).cloned().collect()) + } + async fn fetch_quote(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { // Prevent swaps on unsupported chains - if !self.support_chain(request.from_asset.chain) { + if !self.support_chain(&request.from_asset.chain) { return Err(SwapperError::NotSupportedChain); } let wallet_address = Address::parse_checksummed(&request.wallet_address, None).map_err(|_| SwapperError::InvalidAddress { address: request.wallet_address.clone(), })?; - let deployment = get_deployment_by_chain(request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; + let deployment = get_deployment_by_chain(&request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; let (evm_chain, token_in, token_out, amount_in) = Self::parse_request(request)?; _ = evm_chain.weth_contract().ok_or(SwapperError::NotSupportedChain)?; @@ -359,7 +359,7 @@ impl GemSwapProvider for UniswapV3 { }) .collect(); - let responses = self.jsonrpc_call(ð_calls, provider.clone(), request.from_asset.chain).await?; + let responses = self.jsonrpc_call(ð_calls, provider.clone(), &request.from_asset.chain).await?; let mut max_amount_out = U256::from(0); let mut fee_tier_idx = 0; @@ -377,7 +377,7 @@ impl GemSwapProvider for UniswapV3 { if !request.from_asset.is_native() { // Check allowances approval_type = self - .check_approval(wallet_address, &token_in.to_checksum(), amount_in, request.from_asset.chain, provider) + .check_approval(wallet_address, &token_in.to_checksum(), amount_in, &request.from_asset.chain, provider) .await?; } @@ -403,7 +403,7 @@ impl GemSwapProvider for UniswapV3 { async fn fetch_quote_data(&self, quote: &SwapQuote, _provider: Arc, data: FetchQuoteData) -> Result { let request = "e.request; let (_, token_in, token_out, amount_in) = Self::parse_request(request)?; - let deployment = get_deployment_by_chain(request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; + let deployment = get_deployment_by_chain(&request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; let to_amount = U256::from_str("e.to_value).map_err(|_| SwapperError::InvalidAmount)?; let permit: Option = match data { @@ -429,7 +429,10 @@ impl GemSwapProvider for UniswapV3 { #[cfg(test)] mod tests { use super::*; - use crate::swapper::permit2_data::*; + use crate::{ + config::swap_config::{SwapReferralFee, SwapReferralFees}, + swapper::permit2_data::*, + }; use alloy_core::{hex::decode as HexDecode, hex::encode_prefixed as HexEncode}; use alloy_primitives::aliases::U256; @@ -484,10 +487,10 @@ mod tests { let options = GemSwapOptions { slippage_bps: 100, - fee: Some(GemSwapFee { + fee: Some(SwapReferralFees::evm(SwapReferralFee { bps: 25, - address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), - }), + address: "0x3d83ec320541ae96c4c91e9202643870458fb290".into(), + })), preferred_providers: vec![], }; request.options = Some(options); @@ -565,10 +568,10 @@ mod tests { mode: GemSwapMode::ExactIn, options: Some(GemSwapOptions { slippage_bps: 100, - fee: Some(GemSwapFee { + fee: Some(SwapReferralFees::evm(SwapReferralFee { bps: 25, address: "0x3d83ec320541ae96c4c91e9202643870458fb290".into(), - }), + })), preferred_providers: vec![], }), }; @@ -598,10 +601,10 @@ mod tests { mode: GemSwapMode::ExactIn, options: Some(GemSwapOptions { slippage_bps: 100, - fee: Some(GemSwapFee { + fee: Some(SwapReferralFees::evm(SwapReferralFee { bps: 25, - address: "0x3d83ec320541aE96C4C91E9202643870458fB290".into(), - }), + address: "0x3d83ec320541ae96c4c91e9202643870458fb290".into(), + })), preferred_providers: vec![], }), };