diff --git a/crates/gem_evm/src/jsonrpc.rs b/crates/gem_evm/src/jsonrpc.rs index b7172118..3e39995f 100644 --- a/crates/gem_evm/src/jsonrpc.rs +++ b/crates/gem_evm/src/jsonrpc.rs @@ -47,6 +47,17 @@ impl TransactionObject { data: format!("0x{}", hex::encode(data)), } } + + pub fn new_call_with_from_value(from: &str, to: &str, value: &str, data: Vec) -> Self { + Self { + from: Some(from.to_string()), + to: to.to_string(), + gas: None, + gas_price: None, + value: Some(value.to_string()), + data: format!("0x{}", hex::encode(data)), + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/gem_evm/src/lib.rs b/crates/gem_evm/src/lib.rs index b07ce187..ff6aa37f 100644 --- a/crates/gem_evm/src/lib.rs +++ b/crates/gem_evm/src/lib.rs @@ -11,6 +11,7 @@ pub mod jsonrpc; pub mod lido; pub mod multicall3; pub mod permit2; +pub mod stargate; pub mod thorchain; pub mod uniswap; pub mod weth; diff --git a/crates/gem_evm/src/stargate/contract.rs b/crates/gem_evm/src/stargate/contract.rs new file mode 100644 index 00000000..201988e9 --- /dev/null +++ b/crates/gem_evm/src/stargate/contract.rs @@ -0,0 +1,116 @@ +use alloy_core::sol; +use serde::{Deserialize, Serialize}; + +sol! { + /// Parameters for the OFT send() operation + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct SendParam { + uint32 dstEid; + bytes32 to; + uint256 amountLD; + uint256 minAmountLD; + bytes extraOptions; + bytes composeMsg; + bytes oftCmd; + } + + /// OFT limit information + /// + /// These amounts can change dynamically and are determined by the specific OFT implementation + #[derive(Debug, PartialEq)] + struct OFTLimit { + uint256 minAmountLD; + uint256 maxAmountLD; + } + + /// OFT receipt information containing details about sent and received amounts + #[derive(Debug, PartialEq)] + struct OFTReceipt { + uint256 amountSentLD; + uint256 amountReceivedLD; + } + + /// Detailed information about OFT fees + #[derive(Debug, PartialEq)] + struct OFTFeeDetail { + int256 feeAmountLD; + string description; + } + + /// Structure containing messaging fee information + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct MessagingFee { + uint256 nativeFee; + uint256 lzTokenFee; + } + + /// Receipt for messaging operations + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct MessagingReceipt { + bytes32 guid; + uint64 nonce; + MessagingFee fee; + } + + /// Interface for Stargate cross-chain operations + #[derive(Debug, PartialEq)] + interface IStargate { + /// Provides a quote for messaging fees + /// + /// # Arguments + /// + /// * `_sendParam` - Parameters for the send operation + /// * `_payInLzToken` - Flag indicating whether to pay in LayerZero tokens + /// + /// # Returns + /// + /// * `fee` - Messaging fee information + function quoteSend( + SendParam calldata _sendParam, + bool _payInLzToken + ) external view returns (MessagingFee memory fee); + + /// Provides a quote for sending OFT to another chain + /// + /// # Arguments + /// + /// * `_sendParam` - Parameters for the send operation + /// + /// # Returns + /// + /// * `limit` - Information on OFT transfer limits + /// * `oftFeeDetails` - Details of OFT transaction cost or reward + /// * `receipt` - OFT receipt information indicating token amounts + function quoteOFT( + SendParam calldata _sendParam + ) external view returns ( + OFTLimit memory limit, + OFTFeeDetail[] memory oftFeeDetails, + OFTReceipt memory receipt + ); + + /// Executes the send operation + /// + /// # Arguments + /// + /// * `_sendParam` - Parameters for the send operation + /// * `_fee` - Fee information containing native and LayerZero token fees + /// * `_refundAddress` - Address to receive any excess funds from fees on the source chain + /// + /// # Returns + /// + /// * `msgReceipt` - LayerZero messaging receipt containing: + /// - guid: Unique identifier for the message + /// - nonce: Message nonce + /// - fee: LayerZero fee details + /// * `oftReceipt` - OFT receipt with sent and received amount information + function send( + SendParam calldata _sendParam, + MessagingFee calldata _fee, + address _refundAddress + ) external payable returns ( + MessagingReceipt memory msgReceipt, + OFTReceipt memory oftReceipt + ); + } +} diff --git a/crates/gem_evm/src/stargate/mod.rs b/crates/gem_evm/src/stargate/mod.rs new file mode 100644 index 00000000..2943dbb5 --- /dev/null +++ b/crates/gem_evm/src/stargate/mod.rs @@ -0,0 +1 @@ +pub mod contract; diff --git a/crates/primitives/src/asset.rs b/crates/primitives/src/asset.rs index 7c697373..e8c38f75 100644 --- a/crates/primitives/src/asset.rs +++ b/crates/primitives/src/asset.rs @@ -3,7 +3,7 @@ use typeshare::typeshare; use crate::{asset_id::AssetId, asset_type::AssetType, Chain}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[typeshare(swift = "Sendable")] pub struct Asset { pub id: AssetId, diff --git a/crates/primitives/src/crypto_value_converter.rs b/crates/primitives/src/crypto_value_converter.rs new file mode 100644 index 00000000..76156501 --- /dev/null +++ b/crates/primitives/src/crypto_value_converter.rs @@ -0,0 +1,65 @@ +use num_bigint::BigInt; +use std::str::FromStr; + +pub struct CryptoValueConverter {} + +impl CryptoValueConverter { + pub fn value_from(value: String, decimals: i32) -> BigInt { + let decimals = decimals - 8; + if decimals > 0 { + BigInt::from_str(&value).unwrap() / BigInt::from(10).pow(decimals as u32) + } else { + BigInt::from_str(&value).unwrap() * BigInt::from(10).pow(decimals.unsigned_abs()) + } + } + + pub fn value_to(value: String, decimals: i32) -> BigInt { + let decimals = decimals - 8; + if decimals > 0 { + BigInt::from_str(&value).unwrap() * BigInt::from(10).pow((decimals).unsigned_abs()) + } else { + BigInt::from_str(&value).unwrap() / BigInt::from(10).pow((decimals).unsigned_abs()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use num_bigint::BigInt; + use std::str::FromStr; + + #[test] + fn test_value_from() { + let value = "1000000000".to_string(); + + let result = CryptoValueConverter::value_from(value.clone(), 18); + assert_eq!(result, BigInt::from_str("0").unwrap()); + + let result = CryptoValueConverter::value_from(value.clone(), 10); + assert_eq!(result, BigInt::from_str("10000000").unwrap()); + + let result = CryptoValueConverter::value_from(value.clone(), 6); + assert_eq!(result, BigInt::from_str("100000000000").unwrap()); + + let result = CryptoValueConverter::value_from(value.clone(), 8); + assert_eq!(result, BigInt::from(1000000000)); + } + + #[test] + fn test_value_to() { + let value = "10000000".to_string(); + + let result = CryptoValueConverter::value_to(value.clone(), 18); + assert_eq!(result, BigInt::from_str("100000000000000000").unwrap()); + + let result = CryptoValueConverter::value_to(value.clone(), 10); + assert_eq!(result, BigInt::from(1000000000)); + + let result = CryptoValueConverter::value_to(value.clone(), 6); + assert_eq!(result, BigInt::from(100000)); + + let result = CryptoValueConverter::value_to(value.clone(), 8); + assert_eq!(result, BigInt::from(10000000)); + } +} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 96b20cc2..35770baf 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -18,6 +18,8 @@ pub use self::price::{Price, PriceFull}; pub mod asset; pub mod config; pub use self::config::{ConfigResponse, ConfigVersions, Release}; +pub mod crypto_value_converter; +pub use self::crypto_value_converter::CryptoValueConverter; pub mod currency; pub use self::asset::Asset; pub mod asset_id; diff --git a/gemstone/src/swapper/asset.rs b/gemstone/src/swapper/asset.rs index df25a31c..7876f135 100644 --- a/gemstone/src/swapper/asset.rs +++ b/gemstone/src/swapper/asset.rs @@ -226,4 +226,5 @@ lazy_static! { decimals: 6, asset_type: AssetType::ERC20, }; + } diff --git a/gemstone/src/swapper/mod.rs b/gemstone/src/swapper/mod.rs index daf55e16..dd90f229 100644 --- a/gemstone/src/swapper/mod.rs +++ b/gemstone/src/swapper/mod.rs @@ -17,6 +17,7 @@ pub mod models; pub mod orca; pub mod pancakeswap_aptos; pub mod slippage; +pub mod stargate; pub mod thorchain; pub mod universal_router; @@ -86,6 +87,7 @@ impl GemSwapper { Box::new(jupiter::Jupiter::default()), Box::new(pancakeswap_aptos::PancakeSwapAptos::default()), Box::new(across::Across::default()), + Box::new(stargate::Stargate::new()), ], } } diff --git a/gemstone/src/swapper/models.rs b/gemstone/src/swapper/models.rs index 08fd155a..36b8c4a8 100644 --- a/gemstone/src/swapper/models.rs +++ b/gemstone/src/swapper/models.rs @@ -69,6 +69,7 @@ pub enum SwapProvider { Orca, Jupiter, Across, + Stargate, } #[derive(Debug, Clone, PartialEq, uniffi::Enum)] @@ -88,6 +89,7 @@ impl SwapProvider { Self::Orca => "Orca Whirlpool", Self::Jupiter => "Jupiter", Self::Across => "Across v3", + Self::Stargate => "Stargate v2", } } @@ -100,6 +102,7 @@ impl SwapProvider { Self::Orca => SwapProviderType::OnChain, Self::Jupiter => SwapProviderType::OnChain, Self::Across => SwapProviderType::Bridge, + Self::Stargate => SwapProviderType::Bridge, } } } diff --git a/gemstone/src/swapper/stargate/client.rs b/gemstone/src/swapper/stargate/client.rs new file mode 100644 index 00000000..47311737 --- /dev/null +++ b/gemstone/src/swapper/stargate/client.rs @@ -0,0 +1,191 @@ +use std::{collections::HashMap, str::FromStr, sync::Arc}; + +use alloy_core::sol_types::SolCall; +use alloy_primitives::{hex, Address, Bytes, FixedBytes, U160, U256}; +use gem_evm::{ + jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, + stargate::contract::{IStargate, MessagingFee, OFTReceipt, SendParam}, +}; +use primitives::{AssetId, Chain}; + +use crate::{ + network::{jsonrpc_call, AlienProvider, JsonRpcResult}, + swapper::{SwapQuoteRequest, SwapperError}, +}; + +use super::endpoint::{StargateEndpoint, StargatePool}; + +#[derive(Debug)] +pub struct StargateOFTQuote { + pub receipt: OFTReceipt, +} + +#[derive(Debug, Default)] +pub struct StargateClient { + endpoints: Vec, + chain_endpoints: HashMap, + pools: HashMap, +} + +impl StargateClient { + pub fn from_endpoints(endpoints: Vec) -> Self { + let mut chain_endpoints = HashMap::new(); + let mut pools = HashMap::new(); + + for endpoint in endpoints.iter() { + chain_endpoints.insert(endpoint.id, endpoint.clone()); + for pool in endpoint.pools.iter() { + pools.insert(pool.asset.id.clone(), pool.clone()); + } + } + + Self { + endpoints, + chain_endpoints, + pools, + } + } + + pub fn address_to_bytes32(&self, addr: &str) -> FixedBytes<32> { + FixedBytes::<32>::from(U256::from(U160::from_str(addr).unwrap())) + } + + pub fn get_endpoints(&self) -> Vec<&StargateEndpoint> { + self.endpoints.iter().collect() + } + + pub fn get_endpoint_by_chain(&self, chain: &Chain) -> Result<&StargateEndpoint, SwapperError> { + self.chain_endpoints.get(chain).ok_or(SwapperError::NotSupportedChain) + } + + pub fn get_pool_by_asset_id(&self, asset: &AssetId) -> Result<&StargatePool, SwapperError> { + self.pools.get(asset).ok_or(SwapperError::NotSupportedChain) + } + + pub fn get_decimals_by_asset_id(&self, asset: &AssetId) -> Result { + self.get_pool_by_asset_id(asset).map(|p| p.asset.decimals) + } + + pub fn build_send_param(&self, request: &SwapQuoteRequest) -> Result { + let amount_ld = U256::from_str(request.value.as_str()).unwrap(); + + let destination_endpoint = self.get_endpoint_by_chain(&request.to_asset.chain)?; + + Ok(SendParam { + dstEid: destination_endpoint.endpoint_id, + to: self.address_to_bytes32(request.destination_address.as_str()), + amountLD: amount_ld, + minAmountLD: amount_ld, + extraOptions: Bytes::new(), + composeMsg: Bytes::new(), + oftCmd: Bytes::new(), + }) + } + + pub async fn quote_oft(&self, pool: &StargatePool, send_param: &SendParam, provider: Arc) -> Result { + let calldata = IStargate::quoteOFTCall { + _sendParam: send_param.clone(), + } + .abi_encode(); + + let call = EthereumRpc::Call(TransactionObject::new_call(pool.address.as_str(), calldata), BlockParameter::Latest); + let response: JsonRpcResult = jsonrpc_call(&call, provider, &pool.asset.chain()).await?; + let decoded = hex::decode(response.take()?).map_err(|e| SwapperError::NetworkError { + msg: format!("Unable to hex decode quote oft response: {:?}", e.to_string()), + })?; + let returns = IStargate::quoteOFTCall::abi_decode_returns(&decoded, true).map_err(|e| SwapperError::ABIError { + msg: format!("Unable to abi decode quote oft response: {:?}", e.to_string()), + })?; + + Ok(StargateOFTQuote { receipt: returns.receipt }) + } + + pub async fn quote_send(&self, pool: &StargatePool, send_param: &SendParam, provider: Arc) -> Result { + let calldata = IStargate::quoteSendCall { + _sendParam: send_param.clone(), + _payInLzToken: false, + } + .abi_encode(); + + let call = EthereumRpc::Call(TransactionObject::new_call(pool.address.as_str(), calldata), BlockParameter::Latest); + let response: JsonRpcResult = jsonrpc_call(&call, provider, &pool.asset.chain()).await?; + let decoded = hex::decode(response.take()?).map_err(|e| SwapperError::NetworkError { + msg: format!("Unable to hex decode quote send response: {:?}", e.to_string()), + })?; + let returns = IStargate::quoteSendCall::abi_decode_returns(&decoded, true).map_err(|e| SwapperError::ABIError { + msg: format!("Unable to abi decode quote send response: {:?}", e.to_string()), + })?; + + Ok(returns.fee) + } + + pub fn send(&self, send_param: &SendParam, fee: &MessagingFee, refund_address: &Address) -> Vec { + IStargate::sendCall { + _sendParam: send_param.clone(), + _fee: fee.clone(), + _refundAddress: *refund_address, + } + .abi_encode() + } +} + +#[cfg(test)] +mod tests { + use crate::swapper::stargate::endpoint::STARGATE_ROUTES; + + use super::*; + + #[test] + fn test_get_endpoint_id() { + let stargate = StargateClient::from_endpoints(vec![STARGATE_ROUTES.ethereum.clone()]); + + // Test valid chain + let result = stargate.get_endpoint_by_chain(&Chain::Ethereum); + assert!(result.is_ok()); + + // Test invalid chain + let result = stargate.get_endpoint_by_chain(&Chain::Manta); + assert!(matches!(result, Err(SwapperError::NotSupportedChain))); + } + + #[test] + fn test_address_to_bytes32() { + let stargate = StargateClient::from_endpoints(vec![STARGATE_ROUTES.ethereum.clone()]); + let test_address = "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24"; + let test_result = "0x0000000000000000000000000655c6abda5e2a5241aa08486bd50cf7d475cf24"; + let result = stargate.address_to_bytes32(test_address); + + assert_eq!(result.len(), 32); + assert_eq!(result, FixedBytes::<32>::from_str(test_result).unwrap()); + } + + #[test] + fn test_get_pool() { + let stargate = StargateClient::from_endpoints(vec![STARGATE_ROUTES.ethereum.clone()]); + + // Test with valid asset + let valid_asset = AssetId::from_chain(Chain::Ethereum); // Add appropriate asset details + let result = stargate.get_pool_by_asset_id(&valid_asset); + assert!(result.is_ok()); + + // Test with invalid asset + let invalid_asset = AssetId::from_chain(Chain::Manta); + let result = stargate.get_pool_by_asset_id(&invalid_asset); + assert!(matches!(result, Err(SwapperError::NotSupportedChain))); + } + + #[test] + fn test_get_asset_decimals() { + let stargate = StargateClient::from_endpoints(vec![STARGATE_ROUTES.ethereum.clone()]); + + // Test with valid asset + let valid_asset = AssetId::from_chain(Chain::Ethereum); // Add appropriate asset details + let result = stargate.get_decimals_by_asset_id(&valid_asset); + assert!(result.is_ok()); + + // Test with invalid asset + let invalid_asset = AssetId::from_chain(Chain::Manta); + let result = stargate.get_decimals_by_asset_id(&invalid_asset); + assert!(matches!(result, Err(SwapperError::NotSupportedChain))); + } +} diff --git a/gemstone/src/swapper/stargate/endpoint.rs b/gemstone/src/swapper/stargate/endpoint.rs new file mode 100644 index 00000000..9427369a --- /dev/null +++ b/gemstone/src/swapper/stargate/endpoint.rs @@ -0,0 +1,164 @@ +use lazy_static::lazy_static; +use primitives::{Asset, Chain}; + +use crate::swapper::asset::{ + ARBITRUM_USDC, ARBITRUM_USDT, AVALANCHE_USDC, AVALANCHE_USDT, BASE_USDC, ETHEREUM_USDC, ETHEREUM_USDT, OPTIMISM_USDC, OPTIMISM_USDT, POLYGON_USDC, + POLYGON_USDT, SMARTCHAIN_USDC, SMARTCHAIN_USDT, +}; + +pub const ENDPOINT_ID_ETHEREUM: u32 = 30101; +pub const ENDPOINT_ID_BASE: u32 = 30184; +pub const ENDPOINT_ID_OPTIMISM: u32 = 30111; +pub const ENDPOINT_ID_ARBITRUM: u32 = 30110; +pub const ENDPOINT_ID_POLYGON: u32 = 30109; +pub const ENDPOINT_ID_AVALANCHE: u32 = 30106; +pub const ENDPOINT_ID_LINEA: u32 = 30183; +pub const ENDPOINT_ID_SMARTCHAIN: u32 = 30102; + +#[derive(Clone, Debug, PartialEq)] +pub struct StargatePool { + pub asset: Asset, + pub address: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct StargateEndpoint { + pub id: Chain, + pub pools: Vec, + pub endpoint_id: u32, +} + +#[derive(Clone, Debug)] +pub struct StargateRoutes { + pub ethereum: StargateEndpoint, + pub base: StargateEndpoint, + pub optimism: StargateEndpoint, + pub arbitrum: StargateEndpoint, + pub polygon: StargateEndpoint, + pub avalanche: StargateEndpoint, + pub linea: StargateEndpoint, + pub smartchain: StargateEndpoint, +} + +lazy_static! { + pub static ref STARGATE_ROUTES: StargateRoutes = StargateRoutes { + ethereum: StargateEndpoint { + id: Chain::Ethereum, + pools: vec![ + StargatePool { + asset: Asset::from_chain(Chain::Ethereum), + address: "0x77b2043768d28E9C9aB44E1aBfC95944bcE57931".to_string(), + }, + StargatePool { + asset: ETHEREUM_USDC.clone(), + address: "0xc026395860Db2d07ee33e05fE50ed7bD583189C7".to_string(), + }, + StargatePool { + asset: ETHEREUM_USDT.clone(), + address: "0x933597a323Eb81cAe705C5bC29985172fd5A3973".to_string(), + }, + ], + endpoint_id: ENDPOINT_ID_ETHEREUM, + }, + base: StargateEndpoint { + id: Chain::Base, + pools: vec![ + StargatePool { + asset: Asset::from_chain(Chain::Base), + address: "0xdc181Bd607330aeeBEF6ea62e03e5e1Fb4B6F7C7".to_string(), + }, + StargatePool { + asset: BASE_USDC.clone(), + address: "0x27a16dc786820B16E5c9028b75B99F6f604b5d26".to_string(), + }, + ], + endpoint_id: ENDPOINT_ID_BASE, + }, + optimism: StargateEndpoint { + id: Chain::Optimism, + pools: vec![ + StargatePool { + asset: Asset::from_chain(Chain::Optimism), + address: "0xe8CDF27AcD73a434D661C84887215F7598e7d0d3".to_string(), + }, + StargatePool { + asset: OPTIMISM_USDC.clone(), + address: "0xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0".to_string(), + }, + StargatePool { + asset: OPTIMISM_USDT.clone(), + address: "0x19cFCE47eD54a88614648DC3f19A5980097007dD".to_string(), + }, + ], + endpoint_id: ENDPOINT_ID_OPTIMISM, + }, + arbitrum: StargateEndpoint { + id: Chain::Arbitrum, + pools: vec![ + StargatePool { + asset: Asset::from_chain(Chain::Arbitrum), + address: "0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F".to_string(), + }, + StargatePool { + asset: ARBITRUM_USDC.clone(), + address: "0xe8CDF27AcD73a434D661C84887215F7598e7d0d3".to_string(), + }, + StargatePool { + asset: ARBITRUM_USDT.clone(), + address: "0xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0".to_string(), + }, + ], + endpoint_id: ENDPOINT_ID_ARBITRUM, + }, + polygon: StargateEndpoint { + id: Chain::Polygon, + pools: vec![ + StargatePool { + asset: POLYGON_USDC.clone(), + address: "0x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4".to_string(), + }, + StargatePool { + asset: POLYGON_USDT.clone(), + address: "0xd47b03ee6d86Cf251ee7860FB2ACf9f91B9fD4d7".to_string(), + }, + ], + endpoint_id: ENDPOINT_ID_POLYGON, + }, + avalanche: StargateEndpoint { + id: Chain::AvalancheC, + pools: vec![ + StargatePool { + asset: AVALANCHE_USDC.clone(), + address: "0x5634c4a5FEd09819E3c46D86A965Dd9447d86e47".to_string(), + }, + StargatePool { + asset: AVALANCHE_USDT.clone(), + address: "0x12dC9256Acc9895B076f6638D628382881e62CeE".to_string(), + }, + ], + endpoint_id: ENDPOINT_ID_AVALANCHE, + }, + linea: StargateEndpoint { + id: Chain::Linea, + pools: vec![StargatePool { + asset: Asset::from_chain(Chain::Linea), + address: "0x81F6138153d473E8c5EcebD3DC8Cd4903506B075".to_string(), + }], + endpoint_id: ENDPOINT_ID_LINEA, + }, + smartchain: StargateEndpoint { + id: Chain::SmartChain, + pools: vec![ + StargatePool { + asset: SMARTCHAIN_USDC.clone(), + address: "0x962Bd449E630b0d928f308Ce63f1A21F02576057".to_string(), + }, + StargatePool { + asset: SMARTCHAIN_USDT.clone(), + address: "0x138EB30f73BC423c6455C53df6D89CB01d9eBc63".to_string(), + }, + ], + endpoint_id: ENDPOINT_ID_SMARTCHAIN, + }, + }; +} diff --git a/gemstone/src/swapper/stargate/mod.rs b/gemstone/src/swapper/stargate/mod.rs new file mode 100644 index 00000000..c7f32144 --- /dev/null +++ b/gemstone/src/swapper/stargate/mod.rs @@ -0,0 +1,4 @@ +mod client; +mod endpoint; +pub mod provider; +pub use provider::Stargate; diff --git a/gemstone/src/swapper/stargate/provider.rs b/gemstone/src/swapper/stargate/provider.rs new file mode 100644 index 00000000..80fdb1fb --- /dev/null +++ b/gemstone/src/swapper/stargate/provider.rs @@ -0,0 +1,222 @@ +use std::str::FromStr; +use std::sync::Arc; + +use alloy_primitives::{hex, Address, U256}; +use async_trait::async_trait; +use gem_evm::stargate::contract::{MessagingFee, SendParam}; +use primitives::{Chain, CryptoValueConverter}; +use serde::{Deserialize, Serialize}; + +use crate::{ + network::AlienProvider, + swapper::{ + approval::check_approval_erc20, slippage::apply_slippage_in_bp, ApprovalType, FetchQuoteData, GemSwapProvider, SwapChainAsset, SwapProvider, + SwapProviderData, SwapQuote, SwapQuoteData, SwapQuoteRequest, SwapRoute, SwapperError, + }, +}; + +use super::{client::StargateClient, endpoint::STARGATE_ROUTES}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct StargateRouteData { + send_param: SendParam, + fee: MessagingFee, + refund_address: String, +} + +#[derive(Debug, Default)] +pub struct Stargate { + client: StargateClient, +} + +impl Stargate { + pub fn new() -> Self { + let endpoints = vec![ + STARGATE_ROUTES.ethereum.clone(), + STARGATE_ROUTES.base.clone(), + STARGATE_ROUTES.optimism.clone(), + STARGATE_ROUTES.arbitrum.clone(), + STARGATE_ROUTES.polygon.clone(), + STARGATE_ROUTES.avalanche.clone(), + STARGATE_ROUTES.linea.clone(), + STARGATE_ROUTES.smartchain.clone(), + ]; + + let client = StargateClient::from_endpoints(endpoints); + + Self { client } + } +} + +#[async_trait] +impl GemSwapProvider for Stargate { + fn provider(&self) -> SwapProvider { + SwapProvider::Stargate + } + + fn supported_assets(&self) -> Vec { + self.client + .get_endpoints() + .iter() + .map(|x| SwapChainAsset::Assets(x.id, x.pools.iter().map(|y| y.asset.id.clone()).collect())) + .collect() + } + + async fn fetch_quote(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { + let from_asset = &request.from_asset; + let to_asset = &request.to_asset; + + if from_asset.is_native() && !to_asset.is_native() { + return Err(SwapperError::NotSupportedPair); + } + + let pool = self.client.get_pool_by_asset_id(&request.from_asset)?; + let initial_send_param = self.client.build_send_param(request)?; + + let oft_quote = self.client.quote_oft(pool, &initial_send_param, provider.clone()).await?; + let min_amount_ld = apply_slippage_in_bp(&oft_quote.receipt.amountReceivedLD, request.options.slippage_bps); + let final_send_param = SendParam { + amountLD: initial_send_param.amountLD, + minAmountLD: min_amount_ld, + ..initial_send_param + }; + let messaging_fee = self.client.quote_send(pool, &final_send_param, provider.clone()).await?; + + let approval = if request.from_asset.is_token() { + check_approval_erc20( + request.wallet_address.clone(), + request.from_asset.token_id.clone().unwrap(), + pool.address.clone(), + final_send_param.amountLD, + provider.clone(), + &request.from_asset.chain, + ) + .await? + } else { + ApprovalType::None + }; + + let route_data = StargateRouteData { + send_param: final_send_param.clone(), + fee: messaging_fee, + refund_address: request.wallet_address.to_string(), + }; + + let from_decimals = self.client.get_decimals_by_asset_id(&request.from_asset)?; + let to_decimals = self.client.get_decimals_by_asset_id(&request.to_asset)?; + let mut to_value = CryptoValueConverter::value_from(oft_quote.receipt.amountReceivedLD.to_string(), from_decimals); + to_value = CryptoValueConverter::value_to(to_value.to_string(), to_decimals); + + Ok(SwapQuote { + from_value: request.value.to_string(), + to_value: to_value.to_string(), + data: SwapProviderData { + provider: self.provider(), + routes: vec![SwapRoute { + input: request.from_asset.clone(), + output: request.to_asset.clone(), + route_data: serde_json::to_string(&route_data).unwrap_or_default(), + gas_estimate: None, + }], + suggested_slippage_bps: None, + }, + approval, + request: request.clone(), + }) + } + + async fn fetch_quote_data(&self, quote: &SwapQuote, _provider: Arc, _data: FetchQuoteData) -> Result { + let pool = self.client.get_pool_by_asset_id("e.request.from_asset)?; + let route_data: StargateRouteData = serde_json::from_str("e.data.routes.first().unwrap().route_data).map_err(|_| SwapperError::InvalidRoute)?; + let calldata = self.client.send( + &route_data.send_param, + &route_data.fee, + &Address::from_str(route_data.refund_address.as_str()).unwrap(), + ); + + let amount = if quote.request.from_asset.is_native() { + route_data.send_param.amountLD + } else { + U256::ZERO + }; + let value = route_data.fee.nativeFee + amount; + + let quote_data = SwapQuoteData { + to: pool.address.clone(), + value: value.to_string(), + data: hex::encode_prefixed(calldata.clone()), + }; + + println!("value: {:?}", value); + println!("fee: {:?}", quote_data); + + Ok(quote_data) + } + + async fn get_transaction_status(&self, _chain: Chain, _transaction_hash: &str, _provider: Arc) -> Result { + // TODO: implement onchain scanner solution + todo!() + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use alloy_primitives::U256; + use primitives::AssetId; + + use crate::{ + config::swap_config::SwapReferralFees, + network::mock::AlienProviderMock, + swapper::{asset::BASE_USDC, GemSwapMode, GemSwapOptions}, + }; + + use super::*; + + #[test] + fn should_contain_all_endpoints() { + let stargate = Stargate::new(); + assert_eq!( + stargate.client.get_endpoints(), + vec![ + &STARGATE_ROUTES.ethereum, + &STARGATE_ROUTES.base, + &STARGATE_ROUTES.optimism, + &STARGATE_ROUTES.arbitrum, + &STARGATE_ROUTES.polygon, + &STARGATE_ROUTES.avalanche, + &STARGATE_ROUTES.linea, + &STARGATE_ROUTES.smartchain, + ] + ); + } + + #[tokio::test] + async fn test_native_to_erc20_quote() { + let stargate = Stargate::new(); + let request = SwapQuoteRequest { + wallet_address: "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24".to_string(), + from_asset: AssetId::from_chain(Chain::Ethereum), + to_asset: BASE_USDC.id.clone(), + value: U256::from(1).to_string(), + mode: GemSwapMode::ExactIn, + destination_address: "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24".to_string(), + options: GemSwapOptions { + slippage_bps: 100, + fee: Some(SwapReferralFees::default()), + preferred_providers: vec![], + }, + }; + + let mock = AlienProviderMock { + response: String::from("Hello"), + timeout: Duration::from_millis(100), + }; + + let result = stargate.fetch_quote(&request, Arc::new(mock)).await; + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), SwapperError::NotSupportedPair); + } +} diff --git a/gemstone/src/swapper/thorchain/mod.rs b/gemstone/src/swapper/thorchain/mod.rs index 0f36a375..59d3d466 100644 --- a/gemstone/src/swapper/thorchain/mod.rs +++ b/gemstone/src/swapper/thorchain/mod.rs @@ -5,8 +5,6 @@ mod model; mod provider; use chain::THORChainName; -use num_bigint::BigInt; -use std::str::FromStr; #[derive(Debug, Default)] pub struct ThorChain {} @@ -22,24 +20,6 @@ impl ThorChain { } memo } - - 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()) - } - } } #[cfg(test)] @@ -57,42 +37,4 @@ mod tests { let result = thorchain.data(THORChainName::Bitcoin, memo.clone()); assert_eq!(result, memo); } - - #[test] - fn test_value_from() { - let thorchain = ThorChain::default(); - - 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::default(); - - 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/provider.rs b/gemstone/src/swapper/thorchain/provider.rs index 5fe53386..8f2e75bb 100644 --- a/gemstone/src/swapper/thorchain/provider.rs +++ b/gemstone/src/swapper/thorchain/provider.rs @@ -13,7 +13,7 @@ use alloy_primitives::Address; use alloy_primitives::U256; use async_trait::async_trait; use gem_evm::thorchain::contracts::RouterInterface; -use primitives::Chain; +use primitives::{Chain, CryptoValueConverter}; use std::str::FromStr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -57,7 +57,7 @@ impl GemSwapProvider for ThorChain { let from_asset = THORChainAsset::from_asset_id(request.clone().from_asset).ok_or(SwapperError::NotSupportedAsset)?; let to_asset = THORChainAsset::from_asset_id(request.clone().to_asset).ok_or(SwapperError::NotSupportedAsset)?; - let value = self.value_from(request.clone().value, from_asset.decimals as i32); + let value = CryptoValueConverter::value_from(request.clone().value, from_asset.decimals as i32); let fee = request.options.clone().fee.unwrap_or_default().thorchain; let quote = client @@ -73,7 +73,7 @@ impl GemSwapProvider for ThorChain { ) .await?; - let to_value = self.value_to(quote.expected_amount_out, to_asset.decimals as i32); + let to_value = CryptoValueConverter::value_to(quote.expected_amount_out, to_asset.decimals as i32); let approval: ApprovalType = { if from_asset.use_evm_router() { diff --git a/gemstone/tests/integration_test.rs b/gemstone/tests/integration_test.rs index 550a80fa..97b7c2e0 100644 --- a/gemstone/tests/integration_test.rs +++ b/gemstone/tests/integration_test.rs @@ -10,6 +10,7 @@ mod tests { }; use primitives::{AssetId, Chain}; use reqwest::Client; + use stargate::Stargate; use std::{collections::HashMap, sync::Arc, time::SystemTime}; pub fn print_json(bytes: &[u8]) { @@ -179,4 +180,45 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_stargate_quote() -> Result<(), SwapperError> { + let swap_provider = Stargate::new(); + let network_provider = Arc::new(NativeProvider::default()); + let mut options = GemSwapOptions { + slippage_bps: 100, + fee: Some(SwapReferralFees::evm(SwapReferralFee { + bps: 25, + address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), + })), + preferred_providers: vec![], + }; + options.fee.as_mut().unwrap().evm_bridge = SwapReferralFee { + bps: 25, + address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), + }; + + let request = SwapQuoteRequest { + from_asset: AssetId::from_chain(Chain::Optimism), + to_asset: AssetId::from_chain(Chain::Base), + wallet_address: "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24".into(), + destination_address: "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24".into(), + value: "20000000000000000".into(), // 0.02 ETH + mode: GemSwapMode::ExactIn, + options, + }; + + let now = SystemTime::now(); + let quote = swap_provider.fetch_quote(&request, network_provider.clone()).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, network_provider.clone(), FetchQuoteData::None).await?; + println!("<== quote_data: {:?}", quote_data); + + Ok(()) + } }