diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0eed5ba79..68ec089dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,6 @@ jobs: - name: Build all integration tests only run: | - just --unstable gemstone build-integration-tests just --unstable build-integration-tests - name: Check gemstone dependencies diff --git a/AGENTS.md b/AGENTS.md index b2b7bfa96..dec3cfe14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Gem Wallet Core is a Rust-based cryptocurrency wallet backend engine supporting ### Cross-Platform Library (`gemstone/`) Shared Rust library compiled to iOS Swift Package and Android AAR using UniFFI bindings. Contains blockchain RPC clients, swap integrations, payment URI decoding, and message signing. - - Key module: `gemstone::swapper` — swapper module for on-device swap integrations +- Key module: `gemstone::gem_swapper` — swapper module for on-device swap integrations ### Blockchain Support Individual `gem_*` crates for each blockchain with unified RPC client patterns: diff --git a/Cargo.lock b/Cargo.lock index 7d84dc9e5..769d8ec7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2889,7 +2889,10 @@ dependencies = [ name = "gem_jsonrpc" version = "1.0.0" dependencies = [ + "async-trait", "gem_client", + "hex", + "primitives", "reqwest", "serde", "serde_json", @@ -3041,6 +3044,8 @@ dependencies = [ name = "gem_tron" version = "1.0.0" dependencies = [ + "alloy-primitives", + "alloy-sol-types", "async-trait", "bs58", "chain_traits", @@ -3085,12 +3090,7 @@ name = "gemstone" version = "0.31.1" dependencies = [ "alloy-primitives", - "alloy-sol-types", - "anyhow", "async-trait", - "base64 0.22.1", - "bcs", - "bigdecimal", "bs58", "chain_traits", "chrono", @@ -3115,19 +3115,12 @@ dependencies = [ "gem_xrp", "hex", "num-bigint", - "num-traits", - "number_formatter", "primitives", - "rand 0.9.2", "reqwest", "serde", "serde_json", "serde_serializers", - "serde_urlencoded", - "solana-primitives", - "strum", - "sui-sdk-types", - "sui-transaction-builder", + "swapper", "tokio", "uniffi", ] @@ -6702,6 +6695,54 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "swapper" +version = "1.0.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "async-trait", + "base64 0.22.1", + "bcs", + "bigdecimal", + "bs58", + "futures", + "gem_algorand", + "gem_aptos", + "gem_bitcoin", + "gem_cardano", + "gem_client", + "gem_evm", + "gem_hash", + "gem_hypercore", + "gem_jsonrpc", + "gem_near", + "gem_polkadot", + "gem_solana", + "gem_stellar", + "gem_sui", + "gem_ton", + "gem_tron", + "gem_xrp", + "hex", + "num-bigint", + "num-traits", + "number_formatter", + "primitives", + "rand 0.9.2", + "reqwest", + "serde", + "serde_json", + "serde_serializers", + "serde_urlencoded", + "solana-primitives", + "strum", + "sui-sdk-types", + "sui-transaction-builder", + "tokio", + "tracing", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 93deefb38..e8fec1e5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "crates/number_formatter", "crates/pricer_dex", "crates/streamer", + "crates/swapper", "crates/tracing", ] diff --git a/apps/api/src/assets/cilent.rs b/apps/api/src/assets/cilent.rs index 55c861512..d0c810126 100644 --- a/apps/api/src/assets/cilent.rs +++ b/apps/api/src/assets/cilent.rs @@ -1,7 +1,7 @@ use std::error::Error; use primitives::{Asset, AssetBasic, AssetFull, AssetId, ChainAddress, NFTCollection, Perpetual}; -use search_index::{ASSETS_INDEX_NAME, AssetDocument, NFTS_INDEX_NAME, NFTDocument, PERPETUALS_INDEX_NAME, PerpetualDocument, SearchIndexClient}; +use search_index::{ASSETS_INDEX_NAME, AssetDocument, NFTDocument, NFTS_INDEX_NAME, PERPETUALS_INDEX_NAME, PerpetualDocument, SearchIndexClient}; use storage::DatabaseClient; pub struct AssetsClient { @@ -84,7 +84,14 @@ impl SearchClient { let assets: Vec = self .client - .search(ASSETS_INDEX_NAME, &request.query, &build_filter(filters), [].as_ref(), request.limit, request.offset) + .search( + ASSETS_INDEX_NAME, + &request.query, + &build_filter(filters), + [].as_ref(), + request.limit, + request.offset, + ) .await?; Ok(assets.into_iter().map(|x| AssetBasic::new(x.asset, x.properties, x.score)).collect()) @@ -93,7 +100,14 @@ impl SearchClient { pub async fn get_perpetuals_search(&self, request: &SearchRequest) -> Result, Box> { let perpetuals: Vec = self .client - .search(PERPETUALS_INDEX_NAME, &request.query, &build_filter(vec![]), [].as_ref(), request.limit, request.offset) + .search( + PERPETUALS_INDEX_NAME, + &request.query, + &build_filter(vec![]), + [].as_ref(), + request.limit, + request.offset, + ) .await?; Ok(perpetuals.into_iter().map(|x| x.perpetual).collect()) @@ -102,7 +116,14 @@ impl SearchClient { pub async fn get_nfts_search(&self, request: &SearchRequest) -> Result, Box> { let nfts: Vec = self .client - .search(NFTS_INDEX_NAME, &request.query, &build_filter(vec![]), [].as_ref(), request.limit, request.offset) + .search( + NFTS_INDEX_NAME, + &request.query, + &build_filter(vec![]), + [].as_ref(), + request.limit, + request.offset, + ) .await?; Ok(nfts.into_iter().map(|x| x.collection).collect()) diff --git a/apps/daemon/src/consumers/mod.rs b/apps/daemon/src/consumers/mod.rs index 16c7d5f82..ffd074b21 100644 --- a/apps/daemon/src/consumers/mod.rs +++ b/apps/daemon/src/consumers/mod.rs @@ -146,7 +146,8 @@ pub async fn run_consumer_fetch_nft_associations(settings: Settings, database: A let nft_client = NFTClient::new(&settings.postgres.url, nft_config).await; let nft_client = Arc::new(Mutex::new(nft_client)); let consumer = FetchNftAssetsAddressesConsumer::new(database.clone(), stream_producer, cacher, nft_client); - streamer::run_consumer::(&name, stream_reader, queue, consumer, ConsumerConfig::default()).await + streamer::run_consumer::(&name, stream_reader, queue, consumer, ConsumerConfig::default()) + .await } pub async fn run_consumer_support(settings: Settings, _database: Arc>) -> Result<(), Box> { diff --git a/apps/daemon/src/worker/search/mod.rs b/apps/daemon/src/worker/search/mod.rs index d0323abef..c4fc975c7 100644 --- a/apps/daemon/src/worker/search/mod.rs +++ b/apps/daemon/src/worker/search/mod.rs @@ -46,9 +46,5 @@ pub async fn jobs(settings: Settings) -> Vec + S } }); - vec![ - Box::pin(assets_index_updater), - Box::pin(perpetuals_index_updater), - Box::pin(nfts_index_updater), - ] + vec![Box::pin(assets_index_updater), Box::pin(perpetuals_index_updater), Box::pin(nfts_index_updater)] } diff --git a/bin/gas-bench/src/client.rs b/bin/gas-bench/src/client.rs index 6fba18eba..8888faa43 100644 --- a/bin/gas-bench/src/client.rs +++ b/bin/gas-bench/src/client.rs @@ -3,7 +3,8 @@ use std::error::Error; use gem_evm::fee_calculator::FeeCalculator; use gem_evm::models::fee::EthereumFeeHistory; use gem_evm::{ether_conv::EtherConv, jsonrpc::EthereumRpc}; -use gemstone::network::{alien_provider::NativeProvider, jsonrpc_client_with_chain}; +use gemstone::alien::reqwest_provider::NativeProvider; +use gemstone::network::jsonrpc_client_with_chain; use num_bigint::BigInt; use primitives::{Chain, PriorityFeeValue, fee::FeePriority}; use std::fmt::Display; diff --git a/bin/gas-bench/src/main.rs b/bin/gas-bench/src/main.rs index 19ceca202..af74758d2 100644 --- a/bin/gas-bench/src/main.rs +++ b/bin/gas-bench/src/main.rs @@ -13,7 +13,7 @@ use crate::{ etherscan::EtherscanClient, gasflow::GasflowClient, }; -use gemstone::network::alien_provider::NativeProvider; +use gemstone::alien::reqwest_provider::NativeProvider; use primitives::fee::FeePriority; #[derive(Debug, Clone)] diff --git a/crates/gem_client/Cargo.toml b/crates/gem_client/Cargo.toml index 574a7265b..f75e75668 100644 --- a/crates/gem_client/Cargo.toml +++ b/crates/gem_client/Cargo.toml @@ -14,4 +14,4 @@ serde_json = { workspace = true } serde_urlencoded = { workspace = true } reqwest = { workspace = true, optional = true } hex = { workspace = true } -tokio = { workspace = true, features = ["time"], optional = true } \ No newline at end of file +tokio = { workspace = true, features = ["time"], optional = true } diff --git a/crates/gem_client/src/retry.rs b/crates/gem_client/src/retry.rs index fb8e51ae0..b9f286871 100644 --- a/crates/gem_client/src/retry.rs +++ b/crates/gem_client/src/retry.rs @@ -5,11 +5,6 @@ use std::time::Duration; #[cfg(feature = "reqwest")] use tokio::time::sleep; -/// Create a retry policy for API requests that handles common HTTP error scenarios -/// -/// NOTE: This uses reqwest's built-in retry mechanism which does NOT implement exponential backoff. -/// It will retry immediately without delays, which may not be suitable for rate limiting scenarios. -/// For rate limiting with proper backoff, use the `retry()` function instead. pub fn retry_policy(host: S, max_retries: u32) -> retry::Builder where S: for<'a> PartialEq<&'a str> + Send + Sync + 'static, @@ -27,46 +22,6 @@ where }) } -/// Retry policy with exponential backoff for rate limiting and transient errors -/// -/// This function provides proper exponential backoff (2^attempt seconds) for handling -/// HTTP errors and other transient failures. Uses async sleep when reqwest -/// feature is enabled, otherwise falls back to blocking sleep. -/// -/// # Arguments -/// * `operation` - A closure that returns a Future to be retried -/// * `max_retries` - Maximum number of retry attempts -/// * `should_retry_fn` - Optional predicate function to determine if error should trigger retry -/// If None, defaults to clearly transient errors (429, 502, 503, 504, throttling) -/// -/// # Example -/// ```no_run -/// use gem_client::retry::{retry, default_should_retry}; -/// -/// #[tokio::main] -/// async fn main() { -/// // Retry on clearly transient errors (429, 502, 503, 504, throttling) - default behavior -/// let result = retry( -/// || async { Ok::<(), String>(()) }, -/// 3, -/// None -/// ).await; -/// -/// // Custom retry logic -/// let result = retry( -/// || async { Ok::<(), String>(()) }, -/// 3, -/// Some(|error: &String| error.contains("429")) -/// ).await; -/// -/// // Use explicit predefined function -/// let result = retry( -/// || async { Ok::<(), String>(()) }, -/// 3, -/// Some(default_should_retry) -/// ).await; -/// } -/// ``` pub async fn retry(operation: F, max_retries: u32, should_retry_fn: Option

) -> Result where F: Fn() -> Fut, diff --git a/crates/gem_evm/src/provider/preload.rs b/crates/gem_evm/src/provider/preload.rs index 3413ae9fb..fd4028898 100644 --- a/crates/gem_evm/src/provider/preload.rs +++ b/crates/gem_evm/src/provider/preload.rs @@ -56,7 +56,7 @@ impl EthereumClient { let gas_estimate = { let estimate = self .estimate_gas( - &input.sender_address, + Some(&input.sender_address), &to, Some(&bigint_to_hex_string(&value)), Some(&bytes_to_hex_string(&data)), diff --git a/crates/gem_evm/src/rpc/ankr/client.rs b/crates/gem_evm/src/rpc/ankr/client.rs index 453f4acbd..e47172d6a 100644 --- a/crates/gem_evm/src/rpc/ankr/client.rs +++ b/crates/gem_evm/src/rpc/ankr/client.rs @@ -7,7 +7,7 @@ use serde_json::json; use crate::rpc::ankr::model::{TokenBalances, Transactions, ankr_chain}; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct AnkrClient { pub chain: EVMChain, rpc_client: GenericJsonRpcClient, diff --git a/crates/gem_evm/src/rpc/client.rs b/crates/gem_evm/src/rpc/client.rs index 481964368..bab3c9fc7 100644 --- a/crates/gem_evm/src/rpc/client.rs +++ b/crates/gem_evm/src/rpc/client.rs @@ -1,10 +1,12 @@ use alloy_primitives::{Address, Bytes, hex}; use gem_client::Client; use gem_jsonrpc::client::JsonRpcClient as GenericJsonRpcClient; -use gem_jsonrpc::types::{JsonRpcError, JsonRpcResult}; +use gem_jsonrpc::types::{ERROR_INTERNAL_ERROR, JsonRpcError, JsonRpcResult}; +use num_bigint::{BigInt, Sign}; use serde::de::DeserializeOwned; use serde_json::json; +use serde_serializers::biguint_from_hex_str; use std::any::TypeId; use std::str::FromStr; @@ -27,7 +29,7 @@ pub const FUNCTION_ERC20_NAME: &str = "0x06fdde03"; pub const FUNCTION_ERC20_SYMBOL: &str = "0x95d89b41"; pub const FUNCTION_ERC20_DECIMALS: &str = "0x313ce567"; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct EthereumClient { pub chain: EVMChain, pub client: GenericJsonRpcClient, @@ -187,6 +189,15 @@ impl EthereumClient { self.client.call("eth_getBalance", params).await } + pub async fn gas_price(&self) -> Result { + let value: String = self.client.call("eth_gasPrice", json!([])).await?; + let biguint = biguint_from_hex_str(&value).map_err(|_| JsonRpcError { + code: ERROR_INTERNAL_ERROR, + message: format!("Failed to parse gas price: {value}"), + })?; + Ok(BigInt::from_biguint(Sign::Plus, biguint)) + } + pub async fn get_chain_id(&self) -> Result { self.client.call("eth_chainId", json!([])).await } @@ -236,12 +247,15 @@ impl EthereumClient { Ok(self.client.batch_call::(calls).await?.extract()) } - pub async fn estimate_gas(&self, from: &str, to: &str, value: Option<&str>, data: Option<&str>) -> Result { + pub async fn estimate_gas(&self, from: Option<&str>, to: &str, value: Option<&str>, data: Option<&str>) -> Result { let mut params_obj = json!({ - "from": from, "to": to }); + if let Some(from) = from { + params_obj["from"] = json!(from); + } + if let Some(value) = value { params_obj["value"] = json!(value); } diff --git a/crates/gem_jsonrpc/Cargo.toml b/crates/gem_jsonrpc/Cargo.toml index 4650bd635..27251185c 100644 --- a/crates/gem_jsonrpc/Cargo.toml +++ b/crates/gem_jsonrpc/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } [features] default = ["types"] types = [] -client = ["dep:gem_client"] +client = ["dep:gem_client", "dep:async-trait", "dep:primitives", "dep:hex"] reqwest = ["client", "gem_client/reqwest", "dep:reqwest"] [dependencies] @@ -15,3 +15,6 @@ serde_json = { workspace = true } gem_client = { path = "../gem_client", optional = true } reqwest = { workspace = true, optional = true } +async-trait = { workspace = true, optional = true } +primitives = { path = "../primitives", optional = true } +hex = { workspace = true, optional = true } diff --git a/crates/gem_jsonrpc/src/lib.rs b/crates/gem_jsonrpc/src/lib.rs index f753549d6..199887f96 100644 --- a/crates/gem_jsonrpc/src/lib.rs +++ b/crates/gem_jsonrpc/src/lib.rs @@ -4,3 +4,8 @@ pub mod types; pub mod client; #[cfg(feature = "client")] pub use client::*; + +#[cfg(feature = "client")] +pub mod rpc; +#[cfg(feature = "client")] +pub use rpc::{HttpMethod, RpcClient, RpcProvider, Target, X_CACHE_TTL}; diff --git a/gemstone/src/network/alien_client.rs b/crates/gem_jsonrpc/src/rpc.rs similarity index 50% rename from gemstone/src/network/alien_client.rs rename to crates/gem_jsonrpc/src/rpc.rs index 1651716e9..6df5e7366 100644 --- a/gemstone/src/network/alien_client.rs +++ b/crates/gem_jsonrpc/src/rpc.rs @@ -1,18 +1,96 @@ -use crate::network::{AlienError, AlienProvider, AlienTarget}; use async_trait::async_trait; -use gem_client::{Client, ClientError, ContentType}; +use gem_client::{Client, ClientError, ContentType, Data}; use primitives::Chain; use serde::{Serialize, de::DeserializeOwned}; -use std::{collections::HashMap, str::FromStr, sync::Arc}; +use serde_json; +use std::{collections::HashMap, fmt::Debug, str::FromStr, sync::Arc}; + +pub const X_CACHE_TTL: &str = "x-cache-ttl"; + +#[derive(Debug, Clone)] +pub struct Target { + pub url: String, + pub method: HttpMethod, + pub headers: Option>, + pub body: Option>, +} + +impl Target { + pub fn get(url: &str) -> Self { + Self { + url: url.into(), + method: HttpMethod::Get, + headers: None, + body: None, + } + } + + pub fn post_json(url: &str, body: serde_json::Value) -> Self { + Self { + url: url.into(), + method: HttpMethod::Post, + headers: Some(HashMap::from([("Content-Type".into(), "application/json".into())])), + body: Some(serde_json::to_vec(&body).expect("Failed to serialize JSON body")), + } + } + + pub fn set_cache_ttl(mut self, ttl: u64) -> Self { + if self.headers.is_none() { + self.headers = Some(HashMap::new()); + } + if let Some(headers) = self.headers.as_mut() { + headers.insert(X_CACHE_TTL.into(), ttl.to_string()); + } + self + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum HttpMethod { + Get, + Post, + Put, + Delete, + Head, + Options, + Patch, +} + +impl From for String { + fn from(value: HttpMethod) -> Self { + match value { + HttpMethod::Get => "GET", + HttpMethod::Post => "POST", + HttpMethod::Put => "PUT", + HttpMethod::Delete => "DELETE", + HttpMethod::Head => "HEAD", + HttpMethod::Options => "OPTIONS", + HttpMethod::Patch => "PATCH", + } + .into() + } +} + +#[async_trait] +pub trait RpcProvider: Send + Sync + Debug { + type Error: std::error::Error + Send + Sync + 'static; + + async fn request(&self, target: Target) -> Result; + async fn batch_request(&self, targets: Vec) -> Result, Self::Error>; + fn get_endpoint(&self, chain: Chain) -> Result; +} #[derive(Debug, Clone)] -pub struct AlienClient { +pub struct RpcClient { base_url: String, - provider: Arc, + provider: Arc>, } -impl AlienClient { - pub fn new(base_url: String, provider: Arc) -> Self { +impl RpcClient +where + E: std::error::Error + Send + Sync + 'static, +{ + pub fn new(base_url: String, provider: Arc>) -> Self { Self { base_url, provider } } @@ -22,7 +100,10 @@ impl AlienClient { } #[async_trait] -impl Client for AlienClient { +impl Client for RpcClient +where + E: std::error::Error + Send + Sync + 'static + std::fmt::Display, +{ async fn get(&self, path: &str) -> Result where R: DeserializeOwned, @@ -36,21 +117,21 @@ impl Client for AlienClient { { let url = self.build_url(path); let target = if let Some(headers) = headers { - AlienTarget { + Target { url, - method: crate::network::AlienHttpMethod::Get, + method: HttpMethod::Get, headers: Some(headers), body: None, } } else { - AlienTarget::get(&url) + Target::get(&url) }; let response_data = self .provider .request(target) .await - .map_err(|e| ClientError::Network(format!("Alien provider error: {e}")))?; + .map_err(|e| ClientError::Network(format!("RPC provider error: {e}")))?; serde_json::from_slice(&response_data).map_err(|e| ClientError::Serialization(format!("Failed to deserialize response: {e}"))) } @@ -88,9 +169,9 @@ impl Client for AlienClient { _ => serde_json::to_vec(body)?, }; - let target = AlienTarget { + let target = Target { url, - method: crate::network::AlienHttpMethod::Post, + method: HttpMethod::Post, headers: Some(request_headers), body: Some(data), }; @@ -99,23 +180,28 @@ impl Client for AlienClient { .provider .request(target) .await - .map_err(|e| ClientError::Network(format!("Alien provider error: {e}")))?; + .map_err(|e| ClientError::Network(format!("RPC provider error: {e}")))?; serde_json::from_slice(&response_data).map_err(|e| ClientError::Serialization(format!("Failed to deserialize response: {e}"))) } } #[async_trait] -impl AlienProvider for AlienClient { - async fn request(&self, target: AlienTarget) -> Result, AlienError> { +impl RpcProvider for RpcClient +where + E: std::error::Error + Send + Sync + 'static, +{ + type Error = E; + + async fn request(&self, target: Target) -> Result { self.provider.request(target).await } - async fn batch_request(&self, targets: Vec) -> Result>, AlienError> { + async fn batch_request(&self, targets: Vec) -> Result, Self::Error> { self.provider.batch_request(targets).await } - fn get_endpoint(&self, chain: Chain) -> Result { + fn get_endpoint(&self, chain: Chain) -> Result { self.provider.get_endpoint(chain) } } diff --git a/gemstone/src/sui/gas_budget.rs b/crates/gem_sui/src/gas_budget.rs similarity index 89% rename from gemstone/src/sui/gas_budget.rs rename to crates/gem_sui/src/gas_budget.rs index 6c749370e..6ebba8268 100644 --- a/gemstone/src/sui/gas_budget.rs +++ b/crates/gem_sui/src/gas_budget.rs @@ -1,4 +1,4 @@ -use super::rpc::models::InspectGasUsed; +use crate::models::InspectGasUsed; use std::cmp::max; pub struct GasBudgetCalculator {} diff --git a/crates/gem_sui/src/lib.rs b/crates/gem_sui/src/lib.rs index bbbb4f3d9..5c1167f23 100644 --- a/crates/gem_sui/src/lib.rs +++ b/crates/gem_sui/src/lib.rs @@ -8,6 +8,7 @@ pub mod provider; pub mod models; +pub mod gas_budget; pub mod jsonrpc; pub mod operations; diff --git a/crates/gem_sui/src/models/coin_asset.rs b/crates/gem_sui/src/models/coin_asset.rs new file mode 100644 index 000000000..5b544caab --- /dev/null +++ b/crates/gem_sui/src/models/coin_asset.rs @@ -0,0 +1,31 @@ +use num_bigint::BigInt; +use serde::{Deserialize, Serialize}; +use serde_serializers::{deserialize_bigint_from_str, deserialize_u64_from_str, serialize_bigint, serialize_u64}; +use sui_transaction_builder::unresolved::Input; +use sui_types::{Address, Digest}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CoinAsset { + pub coin_object_id: Address, + pub coin_type: String, + pub digest: Digest, + #[serde(deserialize_with = "deserialize_bigint_from_str", serialize_with = "serialize_bigint")] + pub balance: BigInt, + #[serde(deserialize_with = "deserialize_u64_from_str", serialize_with = "serialize_u64")] + pub version: u64, +} + +impl CoinAsset { + pub fn to_input(&self) -> Input { + Input::owned(self.coin_object_id, self.version, self.digest) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CoinResponse { + pub data: Vec, + pub next_cursor: Option, + pub has_next_page: bool, +} diff --git a/gemstone/src/sui/rpc/models.rs b/crates/gem_sui/src/models/inspect.rs similarity index 51% rename from gemstone/src/sui/rpc/models.rs rename to crates/gem_sui/src/models/inspect.rs index d785fb567..3a15c9ced 100644 --- a/gemstone/src/sui/rpc/models.rs +++ b/crates/gem_sui/src/models/inspect.rs @@ -1,35 +1,5 @@ -use num_bigint::BigInt; use serde::Deserialize; -use serde_serializers::*; - -use sui_transaction_builder::unresolved::Input; -use sui_types::{Address, Digest}; - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CoinAsset { - pub coin_object_id: Address, - pub coin_type: String, - pub digest: Digest, - #[serde(deserialize_with = "deserialize_bigint_from_str", serialize_with = "serialize_bigint")] - pub balance: BigInt, - #[serde(deserialize_with = "deserialize_u64_from_str", serialize_with = "serialize_u64")] - pub version: u64, -} - -impl CoinAsset { - pub fn to_input(&self) -> Input { - Input::owned(self.coin_object_id, self.version, self.digest) - } -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CoinResponse { - pub data: Vec, - pub next_cursor: Option, - pub has_next_page: bool, -} +use serde_serializers::deserialize_u64_from_str; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/gem_sui/src/models/mod.rs b/crates/gem_sui/src/models/mod.rs index d7b34dad7..e343a568d 100644 --- a/crates/gem_sui/src/models/mod.rs +++ b/crates/gem_sui/src/models/mod.rs @@ -1,12 +1,16 @@ pub mod account; pub mod coin; +pub mod coin_asset; pub mod core; +pub mod inspect; pub mod object_id; pub mod staking; pub mod transaction; pub use coin::*; +pub use coin_asset::{CoinAsset, CoinResponse}; pub use core::*; +pub use inspect::{InspectEffects, InspectEvent, InspectGasUsed, InspectResult}; pub use object_id::ObjectId; pub use staking::*; pub use transaction::*; diff --git a/crates/gem_sui/src/rpc/client.rs b/crates/gem_sui/src/rpc/client.rs index 3beeff457..0e5c40686 100644 --- a/crates/gem_sui/src/rpc/client.rs +++ b/crates/gem_sui/src/rpc/client.rs @@ -1,5 +1,7 @@ use std::error::Error; +#[cfg(feature = "rpc")] +use base64::{Engine as _, engine::general_purpose}; #[cfg(feature = "rpc")] use gem_client::Client; #[cfg(all(feature = "reqwest", not(feature = "rpc")))] @@ -9,13 +11,22 @@ use gem_jsonrpc::client::JsonRpcClient as GenericJsonRpcClient; #[cfg(feature = "rpc")] use num_bigint::BigInt; use primitives::chain::Chain; +#[cfg(feature = "rpc")] +use serde::de::DeserializeOwned; +#[cfg(feature = "rpc")] +use sui_types::Address; use crate::models::SuiCoinMetadata; -#[cfg(feature = "rpc")] -use crate::models::SuiObject; use crate::models::staking::{SuiStakeDelegation, SuiSystemState, SuiValidators}; use crate::models::transaction::{SuiBroadcastTransaction, SuiTransaction}; use crate::models::{Balance, Checkpoint, Digest, Digests, ResultData, TransactionBlocks}; +#[cfg(feature = "rpc")] +use crate::models::{CoinAsset, InspectResult, SuiObject}; +#[cfg(feature = "rpc")] +use crate::{ + SUI_COIN_TYPE, SUI_COIN_TYPE_FULL, + jsonrpc::{SuiData, SuiRpc}, +}; use primitives::transaction_load_metadata::SuiCoin; #[cfg(all(feature = "reqwest", not(feature = "rpc")))] @@ -46,6 +57,25 @@ impl SuiClient { &self.client } + pub async fn rpc_call(&self, rpc: SuiRpc) -> Result> { + Ok(self.client.request(rpc).await?) + } + + pub async fn get_coin_assets(&self, owner: Address) -> Result, Box> { + let mut coins: SuiData> = self.rpc_call(SuiRpc::GetAllCoins { owner: owner.to_string() }).await?; + for coin in &mut coins.data { + if coin.coin_type == SUI_COIN_TYPE { + coin.coin_type = SUI_COIN_TYPE_FULL.into(); + } + } + Ok(coins.data) + } + + pub async fn inspect_transaction_block(&self, sender: &str, tx_data: &[u8]) -> Result> { + let tx_bytes_base64 = general_purpose::STANDARD.encode(tx_data); + self.rpc_call(SuiRpc::InspectTransactionBlock(sender.to_string(), tx_bytes_base64)).await + } + pub async fn get_balance(&self, address: String) -> Result> { Ok(self.client.call("suix_getBalance", serde_json::json!([address])).await?) } diff --git a/crates/gem_tron/Cargo.toml b/crates/gem_tron/Cargo.toml index 5e8ca91b1..fe4fb8f0b 100644 --- a/crates/gem_tron/Cargo.toml +++ b/crates/gem_tron/Cargo.toml @@ -19,6 +19,8 @@ gem_client = { path = "../gem_client", optional = true } chain_traits = { path = "../chain_traits", optional = true } async-trait = { workspace = true, optional = true } futures = { workspace = true, optional = true } +alloy-primitives = { workspace = true, optional = true } +alloy-sol-types = { workspace = true, optional = true } [features] default = ["rpc"] @@ -31,6 +33,8 @@ rpc = [ "dep:chain_traits", "dep:async-trait", "dep:futures", + "dep:alloy-primitives", + "dep:alloy-sol-types", ] reqwest = ["gem_client/reqwest"] chain_integration_tests = ["rpc", "reqwest", "primitives/testkit", "settings/testkit"] diff --git a/crates/gem_tron/src/models/mod.rs b/crates/gem_tron/src/models/mod.rs index 4ba4cf263..ba0f90ced 100644 --- a/crates/gem_tron/src/models/mod.rs +++ b/crates/gem_tron/src/models/mod.rs @@ -115,6 +115,10 @@ pub struct TriggerConstantContractRequest { pub contract_address: String, pub function_selector: String, pub parameter: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub call_value: Option, pub visible: bool, } @@ -124,6 +128,8 @@ pub struct TriggerConstantContractResponse { pub constant_result: Vec, pub result: Option, pub energy_used: u64, + #[serde(default)] + pub energy_penalty: Option, } #[derive(Deserialize, Debug)] diff --git a/crates/gem_tron/src/rpc/client.rs b/crates/gem_tron/src/rpc/client.rs index ab1f740bd..73d52e2c2 100644 --- a/crates/gem_tron/src/rpc/client.rs +++ b/crates/gem_tron/src/rpc/client.rs @@ -4,6 +4,7 @@ use num_bigint::BigUint; use primitives::{Asset, AssetId, asset_type::AssetType, chain::Chain}; use std::{error::Error, str::FromStr}; +use crate::address::TronAddress; use crate::models::{ Block, BlockTransactions, BlockTransactionsInfo, ChainParameter, ChainParametersResponse, Transaction, TransactionReceiptData, TriggerConstantContractRequest, TriggerConstantContractResponse, TronTransactionBroadcast, WitnessesList, @@ -13,8 +14,11 @@ use crate::models::{ }; use crate::rpc::constants::{DECIMALS_SELECTOR, DEFAULT_OWNER_ADDRESS, NAME_SELECTOR, SYMBOL_SELECTOR}; use crate::rpc::trongrid::client::TronGridClient; +use alloy_primitives::Address as AlloyAddress; +use alloy_sol_types::SolCall; use gem_client::Client; use gem_evm::contracts::erc20::{decode_abi_string, decode_abi_uint8}; +use serde_json::Value; #[derive(Clone)] pub struct TronClient { @@ -69,6 +73,8 @@ impl TronClient { contract_address: contract_address.to_string(), function_selector: function_selector.to_string(), parameter: parameter.to_string(), + fee_limit: None, + call_value: None, visible: true, }; @@ -80,6 +86,70 @@ impl TronClient { Ok(response.constant_result[0].clone()) } + + pub async fn get_token_allowance(&self, owner_address: &str, token_address: &str, spender_address: &str) -> Result> { + let owner_hex = TronAddress::to_hex(owner_address).ok_or("Invalid owner address")?; + let spender_hex = TronAddress::to_hex(spender_address).ok_or("Invalid spender address")?; + + let owner_bytes = hex::decode(owner_hex)?; + let spender_bytes = hex::decode(spender_hex)?; + + if owner_bytes.len() <= 1 || spender_bytes.len() <= 1 { + return Err("Invalid Tron address bytes".into()); + } + + let owner = AlloyAddress::from_slice(&owner_bytes[1..]); + let spender = AlloyAddress::from_slice(&spender_bytes[1..]); + let encoded = gem_evm::contracts::erc20::IERC20::allowanceCall { owner, spender }.abi_encode(); + let parameter = hex::encode(&encoded[4..]); + + let result = self + .trigger_constant_contract_with_owner(owner_address, token_address, "allowance(address,address)", ¶meter) + .await?; + let allowance_bytes = hex::decode(result.trim_start_matches("0x"))?; + let allowance = BigUint::from_bytes_be(&allowance_bytes); + Ok(allowance) + } + + pub async fn estimate_energy( + &self, + owner_address: &str, + contract_address: &str, + function_selector: &str, + parameter: &str, + fee_limit: u64, + call_value: u64, + ) -> Result> { + let request_payload = TriggerConstantContractRequest { + owner_address: owner_address.to_string(), + contract_address: contract_address.to_string(), + function_selector: function_selector.to_string(), + parameter: parameter.to_string(), + fee_limit: Some(fee_limit), + call_value: Some(call_value), + visible: true, + }; + + let response: Value = self.client.post("/wallet/triggerconstantcontract", &request_payload, None).await?; + + if let Some(result_obj) = response.get("result") { + let is_success = result_obj.get("result").and_then(|value| value.as_bool()).unwrap_or(false); + if !is_success { + let code = result_obj.get("code").and_then(|v| v.as_str()).unwrap_or_default(); + let message_hex = result_obj.get("message").and_then(|v| v.as_str()).unwrap_or_default(); + let message = hex::decode(message_hex) + .ok() + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_else(|| message_hex.to_string()); + return Err(format!("Estimate energy failed. Code: {}, Message: {}", code, message).into()); + } + } + + let energy_used = response.get("energy_used").and_then(|value| value.as_u64()).unwrap_or_default(); + let energy_penalty = response.get("energy_penalty").and_then(|value| value.as_u64()).unwrap_or_default(); + + Ok(energy_used + energy_penalty) + } } impl TronClient { @@ -178,6 +248,8 @@ impl TronClient { contract_address, function_selector: "transfer(address,uint256)".to_string(), parameter, + fee_limit: None, + call_value: None, visible: true, }; diff --git a/crates/primitives/src/swap/approval.rs b/crates/primitives/src/swap/approval.rs index 7f5fcc8c4..2e8723cd3 100644 --- a/crates/primitives/src/swap/approval.rs +++ b/crates/primitives/src/swap/approval.rs @@ -31,7 +31,7 @@ pub struct SwapData { pub data: SwapQuoteData, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[typeshare] pub struct QuoteAsset { pub id: String, @@ -86,7 +86,7 @@ pub struct SwapProviderData { pub protocol_name: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[typeshare(swift = "Equatable, Hashable, Sendable")] #[serde(rename_all = "camelCase")] pub enum SwapStatus { diff --git a/crates/swapper/Cargo.toml b/crates/swapper/Cargo.toml new file mode 100644 index 000000000..ee19974d2 --- /dev/null +++ b/crates/swapper/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "swapper" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } + +[features] +default = [] +reqwest_provider = ["dep:reqwest"] +swap_integration_tests = ["reqwest_provider"] + +[dependencies] +primitives = { path = "../primitives" } +gem_solana = { path = "../gem_solana", features = ["rpc"] } +gem_ton = { path = "../gem_ton", features = ["rpc"] } +gem_tron = { path = "../gem_tron", features = ["rpc"] } +gem_evm = { path = "../gem_evm", features = ["rpc"] } +gem_sui = { path = "../gem_sui", features = ["rpc"] } +gem_aptos = { path = "../gem_aptos", features = ["rpc"] } +gem_hash = { path = "../gem_hash" } +gem_jsonrpc = { path = "../gem_jsonrpc" } +gem_client = { path = "../gem_client" } +gem_hypercore = { path = "../gem_hypercore" } +gem_bitcoin = { path = "../gem_bitcoin", features = ["rpc"] } +gem_cardano = { path = "../gem_cardano", features = ["rpc"] } +gem_algorand = { path = "../gem_algorand", features = ["rpc"] } +gem_stellar = { path = "../gem_stellar", features = ["rpc"] } +gem_xrp = { path = "../gem_xrp", features = ["rpc"] } +gem_near = { path = "../gem_near", features = ["rpc"] } +gem_polkadot = { path = "../gem_polkadot", features = ["rpc"] } +serde_serializers = { path = "../serde_serializers" } +number_formatter = { path = "../number_formatter" } + +reqwest = { workspace = true, optional = true } + +bcs.workspace = true +sui-types = { workspace = true } +sui-transaction-builder = { workspace = true } + +strum = { workspace = true } + +base64.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_urlencoded.workspace = true +async-trait.workspace = true +alloy-primitives.workspace = true +alloy-sol-types.workspace = true +hex.workspace = true +num-bigint.workspace = true +num-traits.workspace = true +futures.workspace = true +bs58 = { workspace = true } +solana-primitives = "0.2.1" +bigdecimal.workspace = true +rand.workspace = true +tracing = "0.1.41" + +[dev-dependencies] +tokio.workspace = true + +[[test]] +name = "integration_test" +test = false diff --git a/gemstone/src/swapper/across/api.rs b/crates/swapper/src/across/api.rs similarity index 79% rename from gemstone/src/swapper/across/api.rs rename to crates/swapper/src/across/api.rs index a7815fdf0..3f24ddd5d 100644 --- a/gemstone/src/swapper/across/api.rs +++ b/crates/swapper/src/across/api.rs @@ -1,7 +1,7 @@ use crate::{ - ethereum::jsonrpc as eth_rpc, - network::{AlienProvider, AlienTarget}, - swapper::SwapperError, + SwapperError, + alien::{RpcProvider, Target}, + client_factory::create_eth_client, }; use primitives::{Chain, swap::SwapStatus}; use serde::{Deserialize, Serialize}; @@ -10,11 +10,11 @@ use std::sync::Arc; #[derive(Debug, Clone)] pub struct AcrossApi { pub url: String, - pub provider: Arc, + pub provider: Arc, } impl AcrossApi { - pub fn new(provider: Arc) -> Self { + pub fn new(provider: Arc) -> Self { Self { url: "https://app.across.to".into(), provider, @@ -45,13 +45,16 @@ impl DepositStatus { impl AcrossApi { pub async fn deposit_status(&self, chain: Chain, tx_hash: &str) -> Result { - let receipt = eth_rpc::fetch_tx_receipt(self.provider.clone(), chain, tx_hash).await?; + let receipt = create_eth_client(self.provider.clone(), chain)? + .get_transaction_receipt(tx_hash) + .await + .map_err(SwapperError::from)?; if receipt.logs.len() < 2 || receipt.logs[1].topics.len() < 4 { return Err(SwapperError::NetworkError("invalid tx receipt".into())); } let deposit_id = receipt.logs[1].topics[3].clone(); let url = format!("{}/deposit/status?originChainId={}&depositId={}", self.url, chain.network_id(), &deposit_id); - let target = AlienTarget::get(&url); + let target = Target::get(&url); let response = self.provider.request(target).await?; let status: DepositStatus = serde_json::from_slice(&response).map_err(SwapperError::from)?; diff --git a/gemstone/src/swapper/across/config_store.rs b/crates/swapper/src/across/config_store.rs similarity index 89% rename from gemstone/src/swapper/across/config_store.rs rename to crates/swapper/src/across/config_store.rs index e3cac6c47..8cf64d82d 100644 --- a/gemstone/src/swapper/across/config_store.rs +++ b/crates/swapper/src/across/config_store.rs @@ -1,6 +1,7 @@ use crate::{ - network::{AlienProvider, JsonRpcClient, JsonRpcResult, jsonrpc_client_with_chain}, - swapper::SwapperError, + SwapperError, + alien::{RpcClient, RpcProvider}, + client_factory::create_client_with_chain, }; use alloy_primitives::{Address, hex::decode as HexDecode}; use alloy_sol_types::SolCall; @@ -9,6 +10,7 @@ use gem_evm::{ jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, multicall3::IMulticall3, }; +use gem_jsonrpc::{JsonRpcClient, types::JsonRpcResult}; use primitives::Chain; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc}; @@ -47,14 +49,14 @@ pub struct TokenConfig { pub struct ConfigStoreClient { pub contract: String, - pub client: JsonRpcClient, + pub client: JsonRpcClient, } impl ConfigStoreClient { - pub fn new(provider: Arc, chain: Chain) -> ConfigStoreClient { + pub fn new(provider: Arc, chain: Chain) -> ConfigStoreClient { ConfigStoreClient { contract: ACROSS_CONFIG_STORE.into(), - client: jsonrpc_client_with_chain(provider.clone(), chain), + client: create_client_with_chain(provider.clone(), chain), } } diff --git a/gemstone/src/swapper/across/hubpool.rs b/crates/swapper/src/across/hubpool.rs similarity index 93% rename from gemstone/src/swapper/across/hubpool.rs rename to crates/swapper/src/across/hubpool.rs index aed1142fb..b0f77b3fd 100644 --- a/gemstone/src/swapper/across/hubpool.rs +++ b/crates/swapper/src/across/hubpool.rs @@ -6,26 +6,28 @@ use num_bigint::{BigInt, Sign}; use primitives::Chain; use crate::{ - network::{AlienProvider, JsonRpcClient, jsonrpc_client_with_chain}, - swapper::SwapperError, + SwapperError, + alien::{RpcClient, RpcProvider}, + client_factory::create_client_with_chain, }; use gem_evm::{ across::{contracts::HubPoolInterface, deployment::ACROSS_HUBPOOL}, jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, multicall3::{IMulticall3, create_call3, decode_call3_return}, }; +use gem_jsonrpc::JsonRpcClient; pub struct HubPoolClient { pub contract: String, - pub client: JsonRpcClient, + pub client: JsonRpcClient, pub chain: Chain, } impl HubPoolClient { - pub fn new(provider: Arc, chain: Chain) -> HubPoolClient { + pub fn new(provider: Arc, chain: Chain) -> HubPoolClient { HubPoolClient { contract: ACROSS_HUBPOOL.into(), - client: jsonrpc_client_with_chain(provider.clone(), chain), + client: create_client_with_chain(provider.clone(), chain), chain, } } diff --git a/gemstone/src/swapper/across/mod.rs b/crates/swapper/src/across/mod.rs similarity index 100% rename from gemstone/src/swapper/across/mod.rs rename to crates/swapper/src/across/mod.rs diff --git a/gemstone/src/swapper/across/provider.rs b/crates/swapper/src/across/provider.rs similarity index 83% rename from gemstone/src/swapper/across/provider.rs rename to crates/swapper/src/across/provider.rs index e010a8f73..5d10de8d7 100644 --- a/gemstone/src/swapper/across/provider.rs +++ b/crates/swapper/src/across/provider.rs @@ -5,20 +5,16 @@ use super::{ hubpool::HubPoolClient, }; use crate::{ - config::swap_config::SwapReferralFee, - debug_println, - ethereum::jsonrpc as eth_rpc, - models::GemApprovalData, - network::AlienProvider, - swapper::{ - Swapper, SwapperError, SwapperProvider, SwapperQuoteData, SwapperSwapResult, - across::{DEFAULT_DEPOSIT_GAS_LIMIT, DEFAULT_FILL_GAS_LIMIT}, - approval::check_approval_erc20, - asset::*, - chainlink::ChainlinkPriceFeed, - eth_address, - models::*, - }, + SwapResult, Swapper, SwapperError, SwapperProvider, SwapperQuoteData, + across::{DEFAULT_DEPOSIT_GAS_LIMIT, DEFAULT_FILL_GAS_LIMIT}, + alien::RpcProvider, + approval::check_approval_erc20, + asset::*, + chainlink::ChainlinkPriceFeed, + client_factory::create_eth_client, + config::ReferralFee, + eth_address, + models::*, }; use alloy_primitives::{ Address, Bytes, U256, @@ -37,28 +33,39 @@ use gem_evm::{ }, contracts::erc20::IERC20, jsonrpc::TransactionObject, + multicall3::IMulticall3, weth::WETH9, }; use num_bigint::{BigInt, Sign}; -use primitives::{AssetId, Chain, EVMChain, swap::SwapStatus}; +use primitives::{AssetId, Chain, EVMChain, swap::ApprovalData, swap::SwapStatus}; +use serde_serializers::biguint_from_hex_str; use std::{fmt::Debug, str::FromStr, sync::Arc}; #[derive(Debug)] pub struct Across { - pub provider: SwapperProviderType, + pub provider: ProviderType, + rpc_provider: Arc, } -impl Default for Across { - fn default() -> Self { +impl Across { + fn bigint_to_u256(value: &BigInt) -> Result { + if value.sign() == Sign::Minus { + return Err(SwapperError::ComputeQuoteError("Negative value provided for gas computation".into())); + } + + let bytes = value.to_bytes_be().1; + Ok(U256::from_be_slice(bytes.as_slice())) + } + + pub fn new(rpc_provider: Arc) -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::Across), + provider: ProviderType::new(SwapperProvider::Across), + rpc_provider, } } -} -impl Across { - pub fn boxed() -> Box { - Box::new(Self::default()) + pub fn boxed(rpc_provider: Arc) -> Box { + Box::new(Self::new(rpc_provider)) } pub fn is_supported_pair(from_asset: &AssetId, to_asset: &AssetId) -> bool { @@ -76,6 +83,30 @@ impl Across { rate_model.clone().into() } + async fn gas_price(&self, chain: Chain) -> Result { + let gas_price = create_eth_client(self.rpc_provider.clone(), chain)?.gas_price().await?; + Self::bigint_to_u256(&gas_price) + } + + async fn multicall3(&self, chain: Chain, calls: Vec) -> Result, SwapperError> { + create_eth_client(self.rpc_provider.clone(), chain)? + .multicall3(calls) + .await + .map_err(|e| SwapperError::NetworkError(e.to_string())) + } + + async fn estimate_gas_transaction(&self, chain: Chain, tx: TransactionObject) -> Result { + let client = create_eth_client(self.rpc_provider.clone(), chain)?; + let gas_hex = client + .estimate_gas(tx.from.as_deref(), &tx.to, tx.value.as_deref(), Some(tx.data.as_str())) + .await + .map_err(SwapperError::from)?; + + let gas_biguint = biguint_from_hex_str(&gas_hex).map_err(|e| SwapperError::NetworkError(format!("Failed to parse gas estimate: {e}")))?; + let gas_bigint = BigInt::from_biguint(Sign::Plus, gas_biguint); + Self::bigint_to_u256(&gas_bigint) + } + /// Return (message, referral_fee) pub fn message_for_multicall_handler( &self, @@ -83,7 +114,7 @@ impl Across { original_output_asset: &AssetId, output_token: &Address, user_address: &Address, - referral_fee: &SwapReferralFee, + referral_fee: &ReferralFee, ) -> (Vec, U256) { if referral_fee.bps == 0 { return (vec![], U256::from(0)); @@ -174,7 +205,6 @@ impl Across { wallet_address: &Address, message: &[u8], deployment: &AcrossDeployment, - provider: Arc, chain: Chain, ) -> Result<(U256, V3RelayData), SwapperError> { let chain_id: u32 = chain.network_id().parse().unwrap(); @@ -206,8 +236,8 @@ impl Across { } .abi_encode(); let tx = TransactionObject::new_call_to_value(deployment.spoke_pool, &value, data); - let gas_limit = eth_rpc::estimate_gas(provider, chain, tx).await; - Ok((gas_limit.unwrap_or(U256::from(DEFAULT_FILL_GAS_LIMIT)), v3_relay_data)) + let gas_limit = self.estimate_gas_transaction(chain, tx).await.unwrap_or(U256::from(DEFAULT_FILL_GAS_LIMIT)); + Ok((gas_limit, v3_relay_data)) } pub fn update_v3_relay_data( @@ -218,7 +248,7 @@ impl Across { original_output_asset: &AssetId, output_token: &Address, timestamp: u32, - referral_fee: &SwapReferralFee, + referral_fee: &ReferralFee, ) -> Result<(), SwapperError> { let (message, _) = self.message_for_multicall_handler(output_amount, original_output_asset, output_token, user_address, referral_fee); @@ -250,7 +280,7 @@ impl Across { #[async_trait] impl Swapper for Across { - fn provider(&self) -> &SwapperProviderType { + fn provider(&self) -> &ProviderType { &self.provider } @@ -282,7 +312,7 @@ impl Swapper for Across { ] } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result { + async fn fetch_quote(&self, request: &QuoteRequest) -> Result { // does not support same chain swap if request.from_asset.chain() == request.to_asset.chain() { return Err(SwapperError::NotSupportedPair); @@ -310,15 +340,15 @@ impl Swapper for Across { let asset_mainnet = asset_mapping.set.iter().find(|x| x.chain == Chain::Ethereum).unwrap(); let mainnet_token = eth_address::normalize_weth_address(asset_mainnet, from_chain)?; - let hubpool_client = HubPoolClient::new(provider.clone(), Chain::Ethereum); - let config_client = ConfigStoreClient::new(provider.clone(), Chain::Ethereum); + let hubpool_client = HubPoolClient::new(self.rpc_provider.clone(), Chain::Ethereum); + let config_client = ConfigStoreClient::new(self.rpc_provider.clone(), Chain::Ethereum); let calls = vec![ hubpool_client.paused_call3(), hubpool_client.sync_call3(&mainnet_token), hubpool_client.pooled_token_call3(&mainnet_token), ]; - let results = eth_rpc::multicall3_call(provider.clone(), &hubpool_client.chain, calls).await?; + let results = self.multicall3(hubpool_client.chain, calls).await?; // Check if protocol is paused let is_paused = hubpool_client.decoded_paused_call3(&results[0])?; @@ -339,16 +369,13 @@ impl Swapper for Across { hubpool_client.get_current_time(), ]; - let eth_price_feed = ChainlinkPriceFeed::new_eth_usd_feed(provider.clone()); + let eth_price_feed = ChainlinkPriceFeed::new_eth_usd_feed(); if !input_is_native { calls.push(eth_price_feed.latest_round_call3()); } - let multicall_req = eth_rpc::multicall3_call(provider.clone(), &hubpool_client.chain, calls); - - let batch_results = futures::join!(token_config_req, multicall_req); - let token_config = batch_results.0?; - let multicall_results = batch_results.1?; + let multicall_results = self.multicall3(hubpool_client.chain, calls).await?; + let token_config = token_config_req.await?; let util_before = hubpool_client.decoded_utilization_call3(&multicall_results[0])?; let util_after = hubpool_client.decoded_utilization_call3(&multicall_results[1])?; @@ -361,13 +388,11 @@ impl Swapper for Across { let lpfee_calc = LpFeeCalculator::new(rate_model); let lpfee_percent = lpfee_calc.realized_lp_fee_pct(&util_before, &util_after, false); let lpfee = fees::multiply(from_amount, lpfee_percent, cost_config.decimals); - debug_println!("lpfee: {}", lpfee); // Calculate relayer fee let relayer_calc = RelayerFeeCalculator::default(); let relayer_fee_percent = relayer_calc.capital_fee_percent(&BigInt::from_str(&request.value).unwrap(), cost_config); let relayer_fee = fees::multiply(from_amount, relayer_fee_percent, cost_config.decimals); - debug_println!("relayer_fee: {}", relayer_fee); let referral_config = request.options.fee.clone().unwrap_or_default().evm_bridge; @@ -376,27 +401,24 @@ impl Swapper for Across { let (message, referral_fee) = self.message_for_multicall_handler(&remain_amount, &original_output_asset, &wallet_address, &output_token, &referral_config); - let gas_price_req = eth_rpc::fetch_gas_price(provider.clone(), request.to_asset.chain()); - let gas_limit_req = self.estimate_gas_limit( - &from_amount, - input_is_native, - &input_asset, - &output_token, - &wallet_address, - &message, - &destination_deployment, - provider.clone(), - request.to_asset.chain(), - ); - - let (tuple, gas_price) = futures::join!(gas_limit_req, gas_price_req); - let (gas_limit, mut v3_relay_data) = tuple?; - let mut gas_fee = gas_limit * gas_price?; + let gas_price = self.gas_price(request.to_asset.chain()).await?; + let (gas_limit, mut v3_relay_data) = self + .estimate_gas_limit( + &from_amount, + input_is_native, + &input_asset, + &output_token, + &wallet_address, + &message, + &destination_deployment, + request.to_asset.chain(), + ) + .await?; + let mut gas_fee = gas_limit * gas_price; if !input_is_native { let eth_price = ChainlinkPriceFeed::decoded_answer(&multicall_results[3])?; gas_fee = Self::calculate_fee_in_token(&gas_fee, ð_price, 6); } - debug_println!("gas_fee: {}", gas_fee); // Check if bridge amount is too small if remain_amount < gas_fee { @@ -418,13 +440,13 @@ impl Swapper for Across { )?; let route_data = HexEncode(v3_relay_data.abi_encode()); - Ok(SwapperQuote { + Ok(Quote { from_value: request.value.clone(), to_value: to_value.to_string(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider().clone(), slippage_bps: request.options.slippage.bps, - routes: vec![SwapperRoute { + routes: vec![Route { input: input_asset.clone(), output: output_asset.clone(), route_data, @@ -436,7 +458,7 @@ impl Swapper for Across { }) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, provider: Arc, data: FetchQuoteData) -> Result { + async fn fetch_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(); @@ -463,7 +485,7 @@ impl Swapper for Across { let input_is_native = quote.request.from_asset.is_native(); let value: &str = if input_is_native { "e.from_value } else { "0" }; - let approval: Option = { + let approval: Option = { if input_is_native { None } else { @@ -472,7 +494,7 @@ impl Swapper for Across { v3_relay_data.inputToken.to_string(), deployment.spoke_pool.into(), v3_relay_data.inputAmount, - provider.clone(), + self.rpc_provider.clone(), &from_chain, ) .await? @@ -486,8 +508,7 @@ impl Swapper for Across { if matches!(data, FetchQuoteData::EstimateGas) { let hex_value = format!("{:#x}", U256::from_str(value).unwrap()); let tx = TransactionObject::new_call_to_value(&to, &hex_value, deposit_v3_call.clone()); - let _gas_limit = eth_rpc::estimate_gas(provider, from_chain, tx).await?; - debug_println!("gas_limit: {:?}", _gas_limit); + let _gas_limit = self.estimate_gas_transaction(from_chain, tx).await?; gas_limit = Some(_gas_limit.to_string()); } @@ -501,8 +522,8 @@ impl Swapper for Across { Ok(quote_data) } - async fn get_swap_result(&self, chain: Chain, transaction_hash: &str, provider: Arc) -> Result { - let api = AcrossApi::new(provider.clone()); + async fn get_swap_result(&self, chain: Chain, transaction_hash: &str) -> Result { + let api = AcrossApi::new(self.rpc_provider.clone()); let status = api.deposit_status(chain, transaction_hash).await?; let swap_status = status.swap_status(); @@ -515,7 +536,7 @@ impl Swapper for Across { SwapStatus::Pending => (destination_chain, None), }; - Ok(SwapperSwapResult { + Ok(SwapResult { status: swap_status, from_chain: chain, from_tx_hash: transaction_hash.to_string(), diff --git a/crates/swapper/src/alien/error.rs b/crates/swapper/src/alien/error.rs new file mode 100644 index 000000000..ea7e96d01 --- /dev/null +++ b/crates/swapper/src/alien/error.rs @@ -0,0 +1,18 @@ +#[derive(Debug, Clone)] +pub enum AlienError { + RequestError { msg: String }, + ResponseError { msg: String }, + SigningError { msg: String }, +} + +impl std::fmt::Display for AlienError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RequestError { msg } => write!(f, "Request error: {}", msg), + Self::ResponseError { msg } => write!(f, "Response error: {}", msg), + Self::SigningError { msg } => write!(f, "Signing error: {}", msg), + } + } +} + +impl std::error::Error for AlienError {} diff --git a/gemstone/src/alien/mock.rs b/crates/swapper/src/alien/mock.rs similarity index 58% rename from gemstone/src/alien/mock.rs rename to crates/swapper/src/alien/mock.rs index 15d490c74..607646ef7 100644 --- a/gemstone/src/alien/mock.rs +++ b/crates/swapper/src/alien/mock.rs @@ -4,14 +4,13 @@ use std::{ time::Duration, }; -use super::{ - AlienError, - provider::{AlienProvider, Data}, - target::AlienTarget, -}; +use super::{AlienError, Target}; +use gem_client::Data; +use gem_jsonrpc::RpcProvider as GenericRpcProvider; use primitives::Chain; -pub struct MockFn(pub Box String + Send + Sync>); +#[allow(unused)] +pub struct MockFn(pub Box String + Send + Sync>); impl fmt::Debug for MockFn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -19,13 +18,15 @@ impl fmt::Debug for MockFn { } } +#[allow(unused)] #[derive(Debug)] -pub struct AlienProviderMock { +pub struct ProviderMock { pub response: MockFn, pub timeout: Duration, } -impl AlienProviderMock { +#[allow(unused)] +impl ProviderMock { pub fn new(string: String) -> Self { Self { response: MockFn(Box::new(move |_| string.clone())), @@ -35,17 +36,19 @@ impl AlienProviderMock { } #[async_trait] -impl AlienProvider for AlienProviderMock { - async fn request(&self, target: AlienTarget) -> Result { +impl GenericRpcProvider for ProviderMock { + type Error = AlienError; + + async fn request(&self, target: Target) -> Result { let responses = self.batch_request(vec![target]).await; responses.map(|responses| responses.first().unwrap().clone()) } - async fn batch_request(&self, targets: Vec) -> Result, AlienError> { + async fn batch_request(&self, targets: Vec) -> Result, Self::Error> { targets.iter().map(|target| Ok(self.response.0(target.clone()).into_bytes())).collect() } - fn get_endpoint(&self, _chain: Chain) -> Result { + fn get_endpoint(&self, _chain: Chain) -> Result { Ok(String::from("http://localhost:8080")) } } diff --git a/crates/swapper/src/alien/mod.rs b/crates/swapper/src/alien/mod.rs new file mode 100644 index 000000000..1c6ff3950 --- /dev/null +++ b/crates/swapper/src/alien/mod.rs @@ -0,0 +1,13 @@ +pub mod error; +pub mod mock; +#[cfg(feature = "reqwest_provider")] +pub mod reqwest_provider; + +pub use error::AlienError; +pub use gem_jsonrpc::{HttpMethod, RpcClient as GenericRpcClient, RpcProvider as GenericRpcProvider, Target, X_CACHE_TTL}; + +pub type RpcClient = GenericRpcClient; + +pub trait RpcProvider: GenericRpcProvider {} + +impl RpcProvider for T where T: GenericRpcProvider {} diff --git a/crates/swapper/src/alien/reqwest_provider.rs b/crates/swapper/src/alien/reqwest_provider.rs new file mode 100644 index 000000000..e95eaf186 --- /dev/null +++ b/crates/swapper/src/alien/reqwest_provider.rs @@ -0,0 +1,108 @@ +use super::{AlienError, HttpMethod, Target}; +use primitives::{Chain, node_config::get_nodes_for_chain}; + +use async_trait::async_trait; +use futures::{TryFutureExt, future::try_join_all}; +use gem_client::Data; +use gem_jsonrpc::RpcProvider as GenericRpcProvider; +use reqwest::Client; + +#[derive(Debug)] +pub struct NativeProvider { + pub client: Client, + debug: bool, +} + +impl NativeProvider { + pub fn new() -> Self { + Self { + client: Client::new(), + debug: true, + } + } + + pub fn set_debug(mut self, debug: bool) -> Self { + self.debug = debug; + self + } +} + +impl Default for NativeProvider { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl GenericRpcProvider for NativeProvider { + type Error = AlienError; + + fn get_endpoint(&self, chain: Chain) -> Result { + let nodes = get_nodes_for_chain(chain); + if nodes.is_empty() { + return Err(Self::Error::ResponseError { + msg: format!("not supported chain: {chain:?}"), + }); + } + Ok(nodes[0].url.clone()) + } + + async fn request(&self, target: Target) -> Result { + if self.debug { + println!("==> request: url: {:?}, method: {:?}", target.url, target.method); + } + let mut req = match target.method { + HttpMethod::Get => self.client.get(target.url), + HttpMethod::Post => self.client.post(target.url), + HttpMethod::Put => self.client.put(target.url), + HttpMethod::Delete => self.client.delete(target.url), + HttpMethod::Head => self.client.head(target.url), + HttpMethod::Patch => self.client.patch(target.url), + HttpMethod::Options => todo!(), + }; + if let Some(headers) = target.headers { + for (key, value) in headers.iter() { + req = req.header(key, value); + } + } + if let Some(body) = target.body { + if self.debug && body.len() <= 4096 { + if let Ok(json) = serde_json::from_slice::(&body) { + println!("=== json: {json:?}"); + } else { + println!("=== body: {:?}", String::from_utf8(body.to_vec()).unwrap()); + } + } + req = req.body(body); + } + + let response = req + .send() + .map_err(|e| Self::Error::ResponseError { + msg: format!("reqwest send error: {e:?}"), + }) + .await?; + let bytes = response + .bytes() + .map_err(|e| Self::Error::ResponseError { + msg: format!("request error: {e:?}"), + }) + .await?; + if self.debug { + println!("<== response body size: {:?}", bytes.len()); + } + if self.debug && bytes.len() <= 4096 { + if let Ok(json) = serde_json::from_slice::(&bytes) { + println!("=== json: {json:?}"); + } else { + println!("=== body: {:?}", String::from_utf8(bytes.to_vec()).unwrap()); + } + } + Ok(bytes.to_vec()) + } + + async fn batch_request(&self, targets: Vec) -> Result, Self::Error> { + let futures = targets.into_iter().map(|target| self.request(target)); + try_join_all(futures).await + } +} diff --git a/gemstone/src/swapper/approval/evm.rs b/crates/swapper/src/approval/evm.rs similarity index 81% rename from gemstone/src/swapper/approval/evm.rs rename to crates/swapper/src/approval/evm.rs index 4865d46e3..2fd20de97 100644 --- a/gemstone/src/swapper/approval/evm.rs +++ b/crates/swapper/src/approval/evm.rs @@ -1,6 +1,12 @@ -use crate::models::GemApprovalData; -use crate::network::{AlienProvider, jsonrpc_client_with_chain}; -use crate::swapper::{Permit2ApprovalData, SwapperError, eth_address, models::ApprovalType}; +use crate::{ + SwapperError, + alien::RpcProvider, + client_factory::create_client_with_chain, + eth_address, + models::{ApprovalType, Permit2ApprovalData}, +}; +use gem_client::Client; +use gem_jsonrpc::client::JsonRpcClient; use alloy_primitives::{Address, U256, hex::decode as HexDecode}; use alloy_sol_types::SolCall; @@ -10,7 +16,7 @@ use gem_evm::{ jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, permit2::IAllowanceTransfer, }; -use primitives::Chain; +use primitives::{Chain, swap::ApprovalData}; use std::{ sync::Arc, time::{SystemTime, UNIX_EPOCH}, @@ -34,26 +40,27 @@ pub enum CheckApprovalType { }, } -pub async fn check_approval_erc20( +pub async fn check_approval_erc20_with_client( owner: String, token: String, spender: String, amount: U256, - provider: Arc, - chain: &Chain, -) -> Result { + client: &JsonRpcClient, +) -> Result +where + C: Client + Clone + std::fmt::Debug + Send + Sync + 'static, +{ let owner: Address = owner.as_str().parse().map_err(|_| SwapperError::InvalidAddress(owner))?; let spender: Address = spender.as_str().parse().map_err(|_| SwapperError::InvalidAddress(spender))?; let allowance_data = IERC20::allowanceCall { owner, spender }.abi_encode(); let allowance_call = EthereumRpc::Call(TransactionObject::new_call(&token, allowance_data), BlockParameter::Latest); - let client = jsonrpc_client_with_chain(provider.clone(), *chain); let result: String = client.request(allowance_call).await.map_err(SwapperError::from)?; let decoded = HexDecode(result).map_err(|_| SwapperError::ABIError("failed to decode allowance_call result".into()))?; let allowance = IERC20::allowanceCall::abi_decode_returns(&decoded).map_err(SwapperError::from)?; if allowance < amount { - return Ok(ApprovalType::Approve(GemApprovalData { + return Ok(ApprovalType::Approve(ApprovalData { token: token.to_string(), spender: spender.to_string(), value: amount.to_string(), @@ -62,15 +69,29 @@ pub async fn check_approval_erc20( Ok(ApprovalType::None) } -pub async fn check_approval_permit2( - permit2_contract: &str, +pub async fn check_approval_erc20( owner: String, token: String, spender: String, amount: U256, - provider: Arc, + provider: Arc, chain: &Chain, ) -> Result { + let client = create_client_with_chain(provider.clone(), *chain); + check_approval_erc20_with_client(owner, token, spender, amount, &client).await +} + +pub async fn check_approval_permit2_with_client( + permit2_contract: &str, + owner: String, + token: String, + spender: String, + amount: U256, + client: &JsonRpcClient, +) -> Result +where + C: Client + Clone + std::fmt::Debug + Send + Sync + 'static, +{ // Check permit2 allowance, spender is universal router let permit2_data = IAllowanceTransfer::allowanceCall { _0: eth_address::parse_str(&owner)?, @@ -80,10 +101,7 @@ pub async fn check_approval_permit2( .abi_encode(); let permit2_call = EthereumRpc::Call(TransactionObject::new_call(permit2_contract, permit2_data), BlockParameter::Latest); - let result: String = jsonrpc_client_with_chain(provider.clone(), *chain) - .request(permit2_call) - .await - .map_err(SwapperError::from)?; + let result: String = client.request(permit2_call).await.map_err(SwapperError::from)?; let decoded = HexDecode(result).unwrap(); let allowance_return = IAllowanceTransfer::allowanceCall::abi_decode_returns(&decoded).map_err(SwapperError::from)?; @@ -109,8 +127,21 @@ pub async fn check_approval_permit2( Ok(ApprovalType::None) } +pub async fn check_approval_permit2( + permit2_contract: &str, + owner: String, + token: String, + spender: String, + amount: U256, + provider: Arc, + chain: &Chain, +) -> Result { + let client = create_client_with_chain(provider.clone(), *chain); + check_approval_permit2_with_client(permit2_contract, owner, token, spender, amount, &client).await +} + #[allow(unused)] -pub async fn check_approval(check_type: CheckApprovalType, provider: Arc, chain: &Chain) -> Result { +pub async fn check_approval(check_type: CheckApprovalType, provider: Arc, chain: &Chain) -> Result { match check_type { CheckApprovalType::ERC20 { owner, token, spender, amount } => check_approval_erc20(owner, token, spender, amount, provider, chain).await, CheckApprovalType::Permit2 { @@ -126,10 +157,7 @@ pub async fn check_approval(check_type: CheckApprovalType, provider: Arc(&body).unwrap(); @@ -186,7 +214,7 @@ mod tests { assert_eq!( result, vec![ - ApprovalType::Approve(GemApprovalData { + ApprovalType::Approve(ApprovalData { token: token.clone(), spender: permit2_contract.clone(), value: amount.to_string() diff --git a/crates/swapper/src/approval/mod.rs b/crates/swapper/src/approval/mod.rs new file mode 100644 index 000000000..92a9e44fb --- /dev/null +++ b/crates/swapper/src/approval/mod.rs @@ -0,0 +1,4 @@ +pub mod evm; +pub mod tron; + +pub use evm::*; diff --git a/crates/swapper/src/approval/tron.rs b/crates/swapper/src/approval/tron.rs new file mode 100644 index 000000000..638dfe7f6 --- /dev/null +++ b/crates/swapper/src/approval/tron.rs @@ -0,0 +1,28 @@ +use crate::{SwapperError, alien::RpcProvider, client_factory::create_tron_client, models::ApprovalType}; +use alloy_primitives::U256; +use num_bigint::BigUint; +use primitives::swap::ApprovalData; +use std::sync::Arc; + +pub async fn check_approval_tron( + owner_address: &str, + token_address: &str, + spender_address: &str, + amount: U256, + provider: Arc, +) -> Result { + let client = create_tron_client(provider.clone()).map_err(|e| SwapperError::NetworkError(e.to_string()))?; + let allowance = client + .get_token_allowance(owner_address, token_address, spender_address) + .await + .map_err(|e| SwapperError::NetworkError(e.to_string()))?; + let amount_big = BigUint::from_bytes_be(&amount.to_be_bytes::<32>()); + if allowance < amount_big { + return Ok(ApprovalType::Approve(ApprovalData { + token: token_address.to_string(), + spender: spender_address.to_string(), + value: amount.to_string(), + })); + } + Ok(ApprovalType::None) +} diff --git a/gemstone/src/swapper/asset.rs b/crates/swapper/src/asset.rs similarity index 100% rename from gemstone/src/swapper/asset.rs rename to crates/swapper/src/asset.rs diff --git a/crates/swapper/src/cetus/api/client.rs b/crates/swapper/src/cetus/api/client.rs new file mode 100644 index 000000000..58e48cc47 --- /dev/null +++ b/crates/swapper/src/cetus/api/client.rs @@ -0,0 +1,48 @@ +use super::models::{CetusPool, Request, Response}; +use crate::{SwapperError, alien::X_CACHE_TTL}; +use gem_client::{Client, ClientError}; +use std::collections::HashMap; + +pub const CETUS_API_URL: &str = "https://api-sui.cetus.zone/v2"; +const POOL_CACHE_TTL: u64 = 60 * 5; // 5 minutes + +#[derive(Clone, Debug)] +pub struct CetusClient +where + C: Client + Clone, +{ + client: C, +} + +impl CetusClient +where + C: Client + Clone, +{ + pub fn new(client: C) -> Self { + Self { client } + } + + pub async fn get_pool_by_token(&self, token_a: &str, token_b: &str) -> Result, SwapperError> { + let request = Request { + display_all_pools: true, + has_mining: true, + no_incentives: true, + coin_type: format!("{token_a},{token_b}"), + }; + let query = serde_urlencoded::to_string(&request).unwrap(); + let path = format!("/sui/stats_pools?{query}"); + let headers = Some(HashMap::from([(X_CACHE_TTL.to_string(), POOL_CACHE_TTL.to_string())])); + + let response: Response = self.client.get_with_headers(&path, headers).await.map_err(map_client_error)?; + + if response.code != 200 { + return Err(SwapperError::NetworkError(format!("API error: {}", response.msg))); + } + + Ok(response.data.lp_list) + } +} + +fn map_client_error(err: ClientError) -> SwapperError { + SwapperError::from(err) +} diff --git a/gemstone/src/swapper/cetus/api/mod.rs b/crates/swapper/src/cetus/api/mod.rs similarity index 100% rename from gemstone/src/swapper/cetus/api/mod.rs rename to crates/swapper/src/cetus/api/mod.rs diff --git a/gemstone/src/swapper/cetus/api/models.rs b/crates/swapper/src/cetus/api/models.rs similarity index 100% rename from gemstone/src/swapper/cetus/api/models.rs rename to crates/swapper/src/cetus/api/models.rs diff --git a/gemstone/src/swapper/cetus/api/test/stats_pool.json b/crates/swapper/src/cetus/api/test/stats_pool.json similarity index 100% rename from gemstone/src/swapper/cetus/api/test/stats_pool.json rename to crates/swapper/src/cetus/api/test/stats_pool.json diff --git a/crates/swapper/src/cetus/default.rs b/crates/swapper/src/cetus/default.rs new file mode 100644 index 000000000..255490631 --- /dev/null +++ b/crates/swapper/src/cetus/default.rs @@ -0,0 +1,26 @@ +use super::{ + api::client::{CETUS_API_URL, CetusClient}, + provider::Cetus, +}; +use crate::{ + Swapper, + alien::{RpcClient, RpcProvider}, + client_factory::create_client_with_chain, +}; +use gem_sui::SuiClient; +use primitives::Chain; +use std::sync::Arc; + +impl Cetus { + pub fn new(rpc_provider: Arc) -> Self { + let http_client = CetusClient::new(RpcClient::new(CETUS_API_URL.into(), rpc_provider.clone())); + let sui_client = Arc::new(SuiClient::new(create_client_with_chain(rpc_provider.clone(), Chain::Sui))); + Self::with_clients(http_client, sui_client) + } +} + +impl Cetus { + pub fn boxed(rpc_provider: Arc) -> Box { + Box::new(Self::new(rpc_provider)) + } +} diff --git a/gemstone/src/swapper/cetus/mod.rs b/crates/swapper/src/cetus/mod.rs similarity index 97% rename from gemstone/src/swapper/cetus/mod.rs rename to crates/swapper/src/cetus/mod.rs index 7b4268d0b..28673cdef 100644 --- a/gemstone/src/swapper/cetus/mod.rs +++ b/crates/swapper/src/cetus/mod.rs @@ -1,4 +1,5 @@ mod api; +pub mod default; mod models; mod provider; mod tx_builder; diff --git a/gemstone/src/swapper/cetus/models.rs b/crates/swapper/src/cetus/models.rs similarity index 96% rename from gemstone/src/swapper/cetus/models.rs rename to crates/swapper/src/cetus/models.rs index d065ebc2b..ce022ba08 100644 --- a/gemstone/src/swapper/cetus/models.rs +++ b/crates/swapper/src/cetus/models.rs @@ -78,13 +78,13 @@ pub struct SharedObject { pub shared_version: u64, } -#[cfg(test)] +#[cfg(all(test, feature = "reqwest_provider"))] mod tests { use super::*; - use crate::network::JsonRpcResponse; - use crate::sui::rpc::{ - CoinAsset, - models::{InspectEvent, InspectResult}, + use gem_jsonrpc::types::JsonRpcResponse; + use gem_sui::{ + jsonrpc::SuiData, + models::{CoinAsset, InspectEvent, InspectResult}, }; use serde_json; diff --git a/gemstone/src/swapper/cetus/provider.rs b/crates/swapper/src/cetus/provider.rs similarity index 84% rename from gemstone/src/swapper/cetus/provider.rs rename to crates/swapper/src/cetus/provider.rs index 6343fe8cb..98f8e7570 100644 --- a/gemstone/src/swapper/cetus/provider.rs +++ b/crates/swapper/src/cetus/provider.rs @@ -5,7 +5,7 @@ use bcs; use futures::join; use num_bigint::BigInt; use num_traits::{FromBytes, ToBytes, ToPrimitive}; -use std::{str::FromStr, sync::Arc}; +use std::{fmt::Debug, str::FromStr, sync::Arc}; use sui_transaction_builder::{Function, Serialized, TransactionBuilder as ProgrammableTransactionBuilder, unresolved::Input}; use sui_types::{Address, Identifier, TypeTag}; @@ -17,43 +17,46 @@ use super::{ tx_builder::TransactionBuilder, }; use crate::{ - debug_println, - network::AlienProvider, - sui::{ - gas_budget::GasBudgetCalculator, - rpc::{ - SuiClient, - models::{InspectEvent, InspectResult}, - }, - }, - swapper::{ - FetchQuoteData, Swapper, SwapperChainAsset, SwapperError, SwapperMode, SwapperProvider, SwapperProviderData, SwapperProviderType, SwapperQuote, - SwapperQuoteData, SwapperQuoteRequest, SwapperRoute, slippage::apply_slippage_in_bp, - }, + FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, Swapper, SwapperChainAsset, SwapperError, SwapperMode, SwapperProvider, + SwapperQuoteData, alien::RpcClient, slippage::apply_slippage_in_bp, }; +use gem_client::Client; use gem_sui::{ - EMPTY_ADDRESS, SUI_COIN_TYPE_FULL, + EMPTY_ADDRESS, SUI_COIN_TYPE_FULL, SuiClient, + gas_budget::GasBudgetCalculator, jsonrpc::{ObjectDataOptions, SuiData, SuiRpc}, - models::TxOutput, + models::{InspectEvent, InspectResult, TxOutput}, }; use primitives::{AssetId, Chain}; -#[derive(Debug)] -pub struct Cetus { - provider: SwapperProviderType, +pub struct Cetus +where + C: Client + Clone + Debug + Send + Sync + 'static, +{ + provider: ProviderType, + cetus_client: CetusClient, + sui_client: Arc>, } -impl Default for Cetus { - fn default() -> Self { - Self { - provider: SwapperProviderType::new(SwapperProvider::Cetus), - } +impl std::fmt::Debug for Cetus +where + C: Client + Clone + Debug + Send + Sync + 'static, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Cetus").field("provider", &self.provider).finish() } } -impl Cetus { - pub fn boxed() -> Box { - Box::new(Self::default()) +impl Cetus +where + C: Client + Clone + Debug + Send + Sync + 'static, +{ + pub fn with_clients(cetus_client: CetusClient, sui_client: Arc>) -> Self { + Self { + provider: ProviderType::new(SwapperProvider::Cetus), + cetus_client, + sui_client, + } } pub fn get_coin_address(asset_id: &AssetId) -> String { @@ -78,14 +81,14 @@ impl Cetus { }) } - async fn fetch_pools_by_coins(&self, coin_a: &str, coin_b: &str, provider: Arc) -> Result, SwapperError> { - let client = CetusClient::new(provider.clone()); - let pools = client + async fn fetch_pools_by_coins(&self, coin_a: &str, coin_b: &str) -> Result, SwapperError> { + let pools: Vec = self + .cetus_client .get_pool_by_token(coin_a, coin_b) .await? - .iter() - .filter_map(|x| if x.object.is_pause { None } else { Some(x.clone()) }) - .collect::>(); + .into_iter() + .filter(|x| !x.object.is_pause) + .collect(); Ok(pools) } @@ -97,7 +100,7 @@ impl Cetus { a2b: bool, buy_amount_in: bool, amount: BigInt, - client: Arc, + client: Arc>, ) -> Result> { let call = self.pre_swap_call(pool, pool_obj, a2b, buy_amount_in, amount)?; let result: InspectResult = client.rpc_call(call).await?; @@ -147,8 +150,11 @@ impl Cetus { } #[async_trait] -impl Swapper for Cetus { - fn provider(&self) -> &SwapperProviderType { +impl Swapper for Cetus +where + C: Client + Clone + Debug + Send + Sync + 'static, +{ + fn provider(&self) -> &ProviderType { &self.provider } @@ -156,12 +162,12 @@ impl Swapper for Cetus { vec![SwapperChainAsset::All(Chain::Sui)] } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result { + async fn fetch_quote(&self, request: &QuoteRequest) -> Result { let from_coin = Self::get_coin_address(&request.from_asset.asset_id()); let to_coin = Self::get_coin_address(&request.to_asset.asset_id()); let amount_in = BigInt::from_str(&request.value).map_err(SwapperError::from)?; - let pools = self.fetch_pools_by_coins(&from_coin, &to_coin, provider.clone()).await?; + let pools = self.fetch_pools_by_coins(&from_coin, &to_coin).await?; if pools.is_empty() { return Err(SwapperError::NoQuoteAvailable); } @@ -172,15 +178,15 @@ impl Swapper for Cetus { sorted_pools.sort_by(|a, b| b.object.liquidity.cmp(&a.object.liquidity)); let top_pools = sorted_pools.iter().take(2).collect::>(); - // Create a single SuiClient that can be reused - let sui_client = Arc::new(SuiClient::new(provider.clone())); + // Reuse the shared SuiClient instance + let sui_client = self.sui_client.clone(); let rpc_call = SuiRpc::GetMultipleObjects( top_pools.iter().map(|pool| pool.address.to_string()).collect(), Some(ObjectDataOptions::default()), ); - let pool_datas: Vec = sui_client.rpc_call(rpc_call).await?; + let pool_datas: Vec = sui_client.rpc_call(rpc_call).await.map_err(|e| SwapperError::NetworkError(e.to_string()))?; let pool_quotes = top_pools .into_iter() @@ -243,13 +249,13 @@ impl Swapper for Cetus { fee_rate: pool.fee.to_string(), }; - Ok(SwapperQuote { + Ok(Quote { from_value: request.value.clone(), to_value: to_value.to_string(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider.clone(), slippage_bps, - routes: vec![SwapperRoute { + routes: vec![Route { input: AssetId::from(Chain::Sui, Some(from_coin.clone())), output: AssetId::from(Chain::Sui, Some(to_coin.clone())), route_data: serde_json::to_string(&route_data).unwrap(), @@ -261,7 +267,7 @@ impl Swapper for Cetus { }) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, provider: Arc, _data: FetchQuoteData) -> Result { + async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { // Validate quote data let route = "e.data.routes.first().ok_or(SwapperError::InvalidRoute)?; let sender_address = quote.request.wallet_address.parse().map_err(SwapperError::from)?; @@ -270,14 +276,17 @@ impl Swapper for Cetus { let from_asset = &route.input; let from_coin = Self::get_coin_address(from_asset); - let sui_client = SuiClient::new(provider.clone()); + let sui_client = self.sui_client.clone(); let cetus_config = self.get_clmm_config()?; // Execute gas_price and coin_assets fetching in parallel let (gas_price_result, all_coin_assets_result) = join!(sui_client.get_gas_price(), sui_client.get_coin_assets(sender_address)); - let gas_price = gas_price_result.map_err(SwapperError::from)?; - let all_coin_assets = all_coin_assets_result.map_err(SwapperError::from)?; + let gas_price_bigint = gas_price_result.map_err(|e| SwapperError::NetworkError(e.to_string()))?; + let gas_price = gas_price_bigint + .to_u64() + .ok_or_else(|| SwapperError::NetworkError("Failed to convert gas price to u64".into()))?; + let all_coin_assets = all_coin_assets_result.map_err(|e| SwapperError::NetworkError(e.to_string()))?; // Prepare swap params for tx building let a2b = from_coin == route_data.coin_a; @@ -306,11 +315,12 @@ impl Swapper for Cetus { // Estimate gas_budget let tx_kind = tx.kind.clone(); let tx_bytes = bcs::to_bytes(&tx_kind).map_err(|e| SwapperError::TransactionError(e.to_string()))?; - let inspect_result = sui_client.inspect_tx_block("e.request.wallet_address, &tx_bytes).await?; + let inspect_result = sui_client + .inspect_transaction_block("e.request.wallet_address, &tx_bytes) + .await + .map_err(|e| SwapperError::NetworkError(e.to_string()))?; let gas_budget = GasBudgetCalculator::gas_budget(&inspect_result.effects.gas_used); - debug_println!("gas_budget: {:?}", gas_budget); - let coin_refs = all_coin_assets .iter() .filter(|x| x.coin_type == SUI_COIN_TYPE_FULL) @@ -331,19 +341,20 @@ impl Swapper for Cetus { } } -#[cfg(test)] +#[cfg(all(test, feature = "reqwest_provider"))] mod tests { use super::*; - use crate::sui::{ - gas_budget, - rpc::{CoinAsset, models::InspectGasUsed}, + use crate::alien::reqwest_provider::NativeProvider; + use gem_sui::{ + gas_budget::GasBudgetCalculator, + models::{CoinAsset, InspectGasUsed}, + tx::decode_transaction, }; - use gem_sui::tx::decode_transaction; use sui_types::{Digest, Transaction, TransactionKind}; #[test] fn test_build_swap_transaction() { - let provider = Cetus::default(); + let provider = Cetus::new(Arc::new(NativeProvider::default())); let cetus_config = provider.get_clmm_config().unwrap(); let sender_address = Address::from_str("0xa9bd0493f9bd1f792a4aedc1f99d54535a75a46c38fd56a8f2c6b7c8d75817a1").unwrap(); @@ -397,7 +408,7 @@ mod tests { storage_cost: 16818800, storage_rebate: 14363316, }; - let gas_budget = gas_budget::GasBudgetCalculator::gas_budget(&gas_used); + let gas_budget = GasBudgetCalculator::gas_budget(&gas_used); gem_sui::tx::fill_tx(&mut ptb, sender_address, 750, gas_budget, all_coins.iter().map(|x| x.to_input()).collect()); let tx = ptb.finish().expect("Failed to build tx"); diff --git a/gemstone/src/swapper/cetus/test/route_data.json b/crates/swapper/src/cetus/test/route_data.json similarity index 100% rename from gemstone/src/swapper/cetus/test/route_data.json rename to crates/swapper/src/cetus/test/route_data.json diff --git a/gemstone/src/swapper/cetus/test/sui_all_coins.json b/crates/swapper/src/cetus/test/sui_all_coins.json similarity index 100% rename from gemstone/src/swapper/cetus/test/sui_all_coins.json rename to crates/swapper/src/cetus/test/sui_all_coins.json diff --git a/gemstone/src/swapper/cetus/test/sui_dev_inspect.json b/crates/swapper/src/cetus/test/sui_dev_inspect.json similarity index 100% rename from gemstone/src/swapper/cetus/test/sui_dev_inspect.json rename to crates/swapper/src/cetus/test/sui_dev_inspect.json diff --git a/gemstone/src/swapper/cetus/test/sui_suip_pool.json b/crates/swapper/src/cetus/test/sui_suip_pool.json similarity index 100% rename from gemstone/src/swapper/cetus/test/sui_suip_pool.json rename to crates/swapper/src/cetus/test/sui_suip_pool.json diff --git a/gemstone/src/swapper/cetus/test/sui_usdc_pool.json b/crates/swapper/src/cetus/test/sui_usdc_pool.json similarity index 100% rename from gemstone/src/swapper/cetus/test/sui_usdc_pool.json rename to crates/swapper/src/cetus/test/sui_usdc_pool.json diff --git a/gemstone/src/swapper/cetus/tx_builder.rs b/crates/swapper/src/cetus/tx_builder.rs similarity index 99% rename from gemstone/src/swapper/cetus/tx_builder.rs rename to crates/swapper/src/cetus/tx_builder.rs index 09807922d..ab9c6a5e1 100644 --- a/gemstone/src/swapper/cetus/tx_builder.rs +++ b/crates/swapper/src/cetus/tx_builder.rs @@ -1,5 +1,4 @@ -use crate::sui::rpc::CoinAsset; -use gem_sui::{ObjectId, SUI_COIN_TYPE_FULL, SUI_FRAMEWORK_PACKAGE_ID, sui_clock_object_input}; +use gem_sui::{ObjectId, SUI_COIN_TYPE_FULL, SUI_FRAMEWORK_PACKAGE_ID, models::CoinAsset, sui_clock_object_input}; use num_bigint::BigInt; use num_traits::ToPrimitive; use std::error::Error; diff --git a/gemstone/src/swapper/chainflip/broker/client.rs b/crates/swapper/src/chainflip/broker/client.rs similarity index 55% rename from gemstone/src/swapper/chainflip/broker/client.rs rename to crates/swapper/src/chainflip/broker/client.rs index 873f92102..7148c2926 100644 --- a/gemstone/src/swapper/chainflip/broker/client.rs +++ b/crates/swapper/src/chainflip/broker/client.rs @@ -2,37 +2,40 @@ use super::{ ChainflipEnvironment, ChainflipIngressEgress, VaultSwapExtras, VaultSwapResponse, model::{ChainflipAsset, DcaParameters, DepositAddressResponse, RefundParameters}, }; -use crate::{ - network::{AlienClient, AlienProvider, JsonRpcClient}, - swapper::SwapperError, -}; -use serde_json::json; -use std::sync::Arc; +use crate::SwapperError; +use gem_client::Client; +use gem_jsonrpc::client::JsonRpcClient; +use serde_json::{Value, json}; +use std::fmt::Debug; -const CHAINFLIP_BROKER_URL: &str = "https://chainflip-broker.io"; -const CHAINFLIP_BROKER_KEY: &str = "ed08651813cc4d4798bf9b953b5d33fb"; +const RPC_PATH: &str = "/rpc"; +const RPC_KEY: &str = "ed08651813cc4d4798bf9b953b5d33fb"; -#[derive(Debug)] -pub struct BrokerClient { - client: JsonRpcClient, +#[derive(Clone, Debug)] +pub struct BrokerClient +where + C: Client + Clone + Debug, +{ + client: JsonRpcClient, } -impl BrokerClient { - pub fn new(provider: Arc) -> Self { - let endpoint = format!("{CHAINFLIP_BROKER_URL}/rpc/{CHAINFLIP_BROKER_KEY}"); - let alien_client = AlienClient::new(endpoint, provider); - Self { - client: JsonRpcClient::new(alien_client), - } +impl BrokerClient +where + C: Client + Clone + Debug, +{ + pub fn new(client: JsonRpcClient) -> Self { + Self { client } } pub async fn get_swap_limits(&self) -> Result { - self.client + let result = self + .client .call_method_with_param("cf_environment", json!([]), Some(60 * 60 * 24 * 30)) .await - .map_err(SwapperError::from) - .map(|x| x.take().map_err(SwapperError::from))? - .map(|x: ChainflipEnvironment| x.ingress_egress) + .map_err(SwapperError::from)?; + + let env: ChainflipEnvironment = result.take().map_err(SwapperError::from)?; + Ok(env.ingress_egress) } pub async fn get_deposit_address( @@ -50,18 +53,20 @@ impl BrokerClient { dst_asset, dst_address, broker_commission_bps, - null, // channel_metadata - boost_fee, // boost_fee - [], // affiliate_fees + Value::Null, + boost_fee, + Vec::::new(), refund_params, dca_params, ]); - self.client + let result = self + .client .call_method_with_param("broker_request_swap_deposit_address", params, None) .await - .map_err(SwapperError::from) - .map(|x| x.take().map_err(SwapperError::from))? + .map_err(SwapperError::from)?; + + result.take().map_err(SwapperError::from) } pub async fn encode_vault_swap( @@ -74,11 +79,11 @@ impl BrokerClient { extra_params: VaultSwapExtras, dca_params: Option, ) -> Result { - let extra_params: serde_json::Value = match extra_params { + let extra_params_json = match extra_params { VaultSwapExtras::Evm(evm) => serde_json::to_value(evm).unwrap(), VaultSwapExtras::Bitcoin(btc) => serde_json::to_value(btc).unwrap(), - VaultSwapExtras::Solana(solana) => serde_json::to_value(solana).unwrap(), - VaultSwapExtras::None => serde_json::json!(null), + VaultSwapExtras::Solana(sol) => serde_json::to_value(sol).unwrap(), + VaultSwapExtras::None => Value::Null, }; let params = json!([ @@ -86,16 +91,23 @@ impl BrokerClient { destination_asset, destination_address, broker_commission, - extra_params, - null, // channel_metadata - boost_fee, // boost_fee - [], // affiliate_fees + extra_params_json, + Value::Null, + boost_fee, + Vec::::new(), dca_params, ]); - self.client + + let result = self + .client .call_method_with_param("broker_request_swap_parameter_encoding", params, None) .await - .map_err(SwapperError::from) - .map(|x| x.take().map_err(SwapperError::from))? + .map_err(SwapperError::from)?; + + result.take().map_err(SwapperError::from) } } + +pub fn build_broker_path(base_url: &str) -> String { + format!("{base_url}{RPC_PATH}/{RPC_KEY}") +} diff --git a/crates/swapper/src/chainflip/broker/mod.rs b/crates/swapper/src/chainflip/broker/mod.rs new file mode 100644 index 000000000..e792b0fcb --- /dev/null +++ b/crates/swapper/src/chainflip/broker/mod.rs @@ -0,0 +1,5 @@ +mod client; +pub mod model; + +pub use client::{BrokerClient, build_broker_path}; +pub use model::*; diff --git a/gemstone/src/swapper/chainflip/broker/model.rs b/crates/swapper/src/chainflip/broker/model.rs similarity index 99% rename from gemstone/src/swapper/chainflip/broker/model.rs rename to crates/swapper/src/chainflip/broker/model.rs index 99fbcd0e1..a1c5dc010 100644 --- a/gemstone/src/swapper/chainflip/broker/model.rs +++ b/crates/swapper/src/chainflip/broker/model.rs @@ -3,7 +3,7 @@ use num_bigint::BigUint; use serde::{Deserialize, Serialize}; use serde_serializers::{deserialize_biguint_from_hex_str, serialize_biguint_to_hex_str}; -use crate::swapper::SwapperError; +use crate::SwapperError; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] pub struct ChainflipAsset { diff --git a/gemstone/src/swapper/chainflip/capitalize.rs b/crates/swapper/src/chainflip/capitalize.rs similarity index 100% rename from gemstone/src/swapper/chainflip/capitalize.rs rename to crates/swapper/src/chainflip/capitalize.rs diff --git a/gemstone/src/swapper/chainflip/client/mod.rs b/crates/swapper/src/chainflip/client/mod.rs similarity index 100% rename from gemstone/src/swapper/chainflip/client/mod.rs rename to crates/swapper/src/chainflip/client/mod.rs diff --git a/gemstone/src/swapper/chainflip/client/model.rs b/crates/swapper/src/chainflip/client/model.rs similarity index 100% rename from gemstone/src/swapper/chainflip/client/model.rs rename to crates/swapper/src/chainflip/client/model.rs diff --git a/crates/swapper/src/chainflip/client/swap.rs b/crates/swapper/src/chainflip/client/swap.rs new file mode 100644 index 000000000..b52ed5a8f --- /dev/null +++ b/crates/swapper/src/chainflip/client/swap.rs @@ -0,0 +1,51 @@ +use super::{ + SwapTxResponse, + model::{QuoteRequest, QuoteResponse}, +}; +use crate::SwapperError; +use gem_client::{Client, ClientError}; +use serde_json::Value; +use serde_urlencoded; +use std::fmt::Debug; + +const QUOTE_PATH: &str = "/v2/quote"; +const SWAP_PATH: &str = "/v2/swap"; + +#[derive(Clone, Debug)] +pub struct ChainflipClient +where + C: Client + Clone + Debug, +{ + client: C, +} + +impl ChainflipClient +where + C: Client + Clone + Debug, +{ + pub fn new(client: C) -> Self { + Self { client } + } + + pub async fn get_quote(&self, request: &QuoteRequest) -> Result, SwapperError> { + let query = serde_urlencoded::to_string(request).map_err(SwapperError::from)?; + let path = format!("{QUOTE_PATH}?{query}"); + let value: Value = self.client.get(&path).await.map_err(map_client_error)?; + + if let Some(message) = value.get("message").and_then(Value::as_str) { + return Err(SwapperError::ComputeQuoteError(message.to_string())); + } + + let quotes = serde_json::from_value(value).map_err(SwapperError::from)?; + Ok(quotes) + } + + pub async fn get_tx_status(&self, tx_hash: &str) -> Result { + let path = format!("{SWAP_PATH}/{tx_hash}"); + self.client.get(&path).await.map_err(map_client_error) + } +} + +fn map_client_error(err: ClientError) -> SwapperError { + SwapperError::from(err) +} diff --git a/gemstone/src/swapper/chainflip/client/test/btc_eth_quote.json b/crates/swapper/src/chainflip/client/test/btc_eth_quote.json similarity index 100% rename from gemstone/src/swapper/chainflip/client/test/btc_eth_quote.json rename to crates/swapper/src/chainflip/client/test/btc_eth_quote.json diff --git a/crates/swapper/src/chainflip/default.rs b/crates/swapper/src/chainflip/default.rs new file mode 100644 index 000000000..b04ec088d --- /dev/null +++ b/crates/swapper/src/chainflip/default.rs @@ -0,0 +1,23 @@ +use super::{ + broker::{BrokerClient, build_broker_path}, + client::ChainflipClient, + provider::ChainflipProvider, +}; +use crate::alien::{RpcClient, RpcProvider}; +use gem_jsonrpc::client::JsonRpcClient; +use std::sync::Arc; + +const CHAINFLIP_API_URL: &str = "https://chainflip-swap.chainflip.io"; +const CHAINFLIP_BROKER_URL: &str = "https://chainflip-broker.io"; + +impl ChainflipProvider { + pub fn new(rpc_provider: Arc) -> Self { + let api_client = RpcClient::new(CHAINFLIP_API_URL.into(), rpc_provider.clone()); + let chainflip_client = ChainflipClient::new(api_client.clone()); + + let broker_endpoint = build_broker_path(CHAINFLIP_BROKER_URL); + let broker_client = BrokerClient::new(JsonRpcClient::new(RpcClient::new(broker_endpoint, rpc_provider.clone()))); + + Self::with_clients(chainflip_client, broker_client, rpc_provider) + } +} diff --git a/gemstone/src/swapper/chainflip/mod.rs b/crates/swapper/src/chainflip/mod.rs similarity index 91% rename from gemstone/src/swapper/chainflip/mod.rs rename to crates/swapper/src/chainflip/mod.rs index 0e6580767..a89e3d5b4 100644 --- a/gemstone/src/swapper/chainflip/mod.rs +++ b/crates/swapper/src/chainflip/mod.rs @@ -1,6 +1,7 @@ pub mod broker; pub mod capitalize; pub mod client; +pub mod default; pub mod model; pub mod price; pub mod provider; diff --git a/gemstone/src/swapper/chainflip/model.rs b/crates/swapper/src/chainflip/model.rs similarity index 100% rename from gemstone/src/swapper/chainflip/model.rs rename to crates/swapper/src/chainflip/model.rs diff --git a/gemstone/src/swapper/chainflip/price.rs b/crates/swapper/src/chainflip/price.rs similarity index 100% rename from gemstone/src/swapper/chainflip/price.rs rename to crates/swapper/src/chainflip/price.rs diff --git a/gemstone/src/swapper/chainflip/provider.rs b/crates/swapper/src/chainflip/provider.rs similarity index 64% rename from gemstone/src/swapper/chainflip/provider.rs rename to crates/swapper/src/chainflip/provider.rs index 4bb2eff31..dcf9920c7 100644 --- a/gemstone/src/swapper/chainflip/provider.rs +++ b/crates/swapper/src/chainflip/provider.rs @@ -1,46 +1,61 @@ use alloy_primitives::{U256, hex}; +use async_trait::async_trait; +use gem_client::Client; use num_bigint::BigUint; use num_traits::ToPrimitive; -use std::{str::FromStr, sync::Arc}; +use std::{fmt::Debug, str::FromStr, sync::Arc}; use super::{ ChainflipRouteData, - broker::{BrokerClient, model::*}, + broker::{ + BrokerClient, ChainflipAsset, DcaParameters, RefundParameters, VaultSwapBtcExtras, VaultSwapEvmExtras, VaultSwapExtras, VaultSwapResponse, + VaultSwapSolanaExtras, + }, capitalize::capitalize_first_letter, - client::{ChainflipClient, QuoteRequest, QuoteResponse}, + client::{ChainflipClient, QuoteRequest as ChainflipQuoteRequest, QuoteResponse}, price::{apply_slippage, price_to_hex_price}, seed::generate_random_seed, tx_builder, }; use crate::{ - config::swap_config::DEFAULT_CHAINFLIP_FEE_BPS, - network::AlienProvider, - swapper::{ - FetchQuoteData, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperProviderData, SwapperProviderType, SwapperQuote, SwapperQuoteData, - SwapperQuoteRequest, SwapperRoute, SwapperSwapResult, - approval::check_approval_erc20, - asset::{ARBITRUM_USDC, ETHEREUM_FLIP, ETHEREUM_USDC, ETHEREUM_USDT, SOLANA_USDC}, - slippage, - }, + FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, + SwapperQuoteData, + alien::RpcProvider, + approval::check_approval_erc20, + asset::{ARBITRUM_USDC, ETHEREUM_FLIP, ETHEREUM_USDC, ETHEREUM_USDT, SOLANA_USDC}, + config::DEFAULT_CHAINFLIP_FEE_BPS, + slippage, }; use primitives::{ChainType, chain::Chain, swap::QuoteAsset}; const DEFAULT_SWAP_ERC20_GAS_LIMIT: u64 = 100_000; #[derive(Debug)] -pub struct ChainflipProvider { - provider: SwapperProviderType, +pub struct ChainflipProvider +where + CX: Client + Clone + Send + Sync + Debug + 'static, + BR: Client + Clone + Send + Sync + Debug + 'static, +{ + provider: ProviderType, + chainflip_client: ChainflipClient, + broker_client: BrokerClient
, + rpc_provider: Arc, } -impl Default for ChainflipProvider { - fn default() -> Self { +impl ChainflipProvider +where + CX: Client + Clone + Send + Sync + Debug + 'static, + BR: Client + Clone + Send + Sync + Debug + 'static, +{ + pub fn with_clients(chainflip_client: ChainflipClient, broker_client: BrokerClient
, rpc_provider: Arc) -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::Chainflip), + provider: ProviderType::new(SwapperProvider::Chainflip), + chainflip_client, + broker_client, + rpc_provider, } } -} -impl ChainflipProvider { fn map_asset_id(asset: &QuoteAsset) -> ChainflipAsset { let asset_id = asset.asset_id(); let chain_name = capitalize_first_letter(asset_id.chain.as_ref()); @@ -53,58 +68,58 @@ impl ChainflipProvider { fn map_chainflip_chain_to_chain(chainflip_chain: &str) -> Option { Chain::from_str(&chainflip_chain.to_lowercase()).ok() } +} - fn get_best_quote(mut quotes: Vec, fee_bps: u32) -> (BigUint, u32, u32, ChainflipRouteData) { - quotes.sort_by(|a, b| b.egress_amount.cmp(&a.egress_amount)); - let quote = "es[0]; - - let egress_amount: BigUint; - let eta_in_seconds: u32; - let slippage_bps: u32; - let boost_fee: Option; - let estimated_price: String; - let dca_parameters: Option; - - // Use boost quote if available - if let Some(boost_quote) = "e.boost_quote { - egress_amount = boost_quote.egress_amount.clone(); - slippage_bps = boost_quote.slippage_bps(); - eta_in_seconds = boost_quote.estimated_duration_seconds as u32; - boost_fee = Some(boost_quote.estimated_boost_fee_bps); - estimated_price = boost_quote.estimated_price.clone(); - dca_parameters = boost_quote.dca_params.as_ref().map(|dca_params| DcaParameters { - number_of_chunks: dca_params.number_of_chunks, - chunk_interval: dca_params.chunk_interval_blocks, - }); - } else { - egress_amount = quote.egress_amount.clone(); - slippage_bps = quote.slippage_bps(); - eta_in_seconds = quote.estimated_duration_seconds as u32; - boost_fee = None; - estimated_price = quote.estimated_price.clone(); - dca_parameters = quote.dca_params.as_ref().map(|dca_params| DcaParameters { - number_of_chunks: dca_params.number_of_chunks, - chunk_interval: dca_params.chunk_interval_blocks, - }); - } +fn get_best_quote(mut quotes: Vec, fee_bps: u32) -> (BigUint, u32, u32, ChainflipRouteData) { + quotes.sort_by(|a, b| b.egress_amount.cmp(&a.egress_amount)); + let quote = "es[0]; + let (egress_amount, slippage_bps, eta_in_seconds, boost_fee, estimated_price, dca_parameters) = if let Some(boost_quote) = "e.boost_quote { ( - egress_amount, - slippage_bps, - eta_in_seconds, - ChainflipRouteData { - boost_fee, - fee_bps, - estimated_price, - dca_parameters, - }, + boost_quote.egress_amount.clone(), + boost_quote.slippage_bps(), + boost_quote.estimated_duration_seconds as u32, + Some(boost_quote.estimated_boost_fee_bps), + boost_quote.estimated_price.clone(), + boost_quote.dca_params.as_ref().map(|dca| DcaParameters { + number_of_chunks: dca.number_of_chunks, + chunk_interval: dca.chunk_interval_blocks, + }), ) - } + } else { + ( + quote.egress_amount.clone(), + quote.slippage_bps(), + quote.estimated_duration_seconds as u32, + None, + quote.estimated_price.clone(), + quote.dca_params.as_ref().map(|dca| DcaParameters { + number_of_chunks: dca.number_of_chunks, + chunk_interval: dca.chunk_interval_blocks, + }), + ) + }; + + ( + egress_amount, + slippage_bps, + eta_in_seconds, + ChainflipRouteData { + boost_fee, + fee_bps, + estimated_price, + dca_parameters, + }, + ) } -#[async_trait::async_trait] -impl Swapper for ChainflipProvider { - fn provider(&self) -> &SwapperProviderType { +#[async_trait] +impl Swapper for ChainflipProvider +where + CX: Client + Clone + Send + Sync + Debug + 'static, + BR: Client + Clone + Send + Sync + Debug + 'static, +{ + fn provider(&self) -> &ProviderType { &self.provider } @@ -120,19 +135,16 @@ impl Swapper for ChainflipProvider { ] } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result { - // Disable swap from BTC until Chainflip scan shows pending transactions + async fn fetch_quote(&self, request: &QuoteRequest) -> Result { if request.from_asset.chain().chain_type() == ChainType::Bitcoin { return Err(SwapperError::NoQuoteAvailable); } let src_asset = Self::map_asset_id(&request.from_asset); let dest_asset = Self::map_asset_id(&request.to_asset); - let chainflip_client = ChainflipClient::new(provider.clone()); let fee_bps = DEFAULT_CHAINFLIP_FEE_BPS; - - let quote_request = QuoteRequest { + let quote_request = ChainflipQuoteRequest { amount: request.value.clone(), src_chain: src_asset.chain.clone(), src_asset: src_asset.asset.clone(), @@ -143,22 +155,20 @@ impl Swapper for ChainflipProvider { broker_commission_bps: Some(fee_bps), }; - let quote_req = chainflip_client.get_quote("e_request); - let quotes = quote_req.await?; - + let quotes = self.chainflip_client.get_quote("e_request).await?; if quotes.is_empty() { return Err(SwapperError::NoQuoteAvailable); } - let (egress_amount, slippage_bps, eta_in_seconds, route_data) = Self::get_best_quote(quotes, fee_bps); + let (egress_amount, slippage_bps, eta_in_seconds, route_data) = get_best_quote(quotes, fee_bps); - let quote = SwapperQuote { + Ok(Quote { from_value: request.value.clone(), to_value: egress_amount.to_string(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider.clone(), slippage_bps, - routes: vec![SwapperRoute { + routes: vec![Route { input: request.from_asset.asset_id(), output: request.to_asset.asset_id(), route_data: serde_json::to_string(&route_data).unwrap(), @@ -167,13 +177,11 @@ impl Swapper for ChainflipProvider { }, eta_in_seconds: Some(eta_in_seconds), request: request.clone(), - }; - Ok(quote) + }) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, provider: Arc, _data: FetchQuoteData) -> Result { + async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let from_asset = quote.request.from_asset.asset_id(); - let broker_client = BrokerClient::new(provider.clone()); let source_asset = Self::map_asset_id("e.request.from_asset); let destination_asset = Self::map_asset_id("e.request.to_asset); @@ -194,7 +202,7 @@ impl Swapper for ChainflipProvider { chain, input_amount: input_amount.clone(), refund_parameters: RefundParameters { - retry_duration: 150, // blocks + retry_duration: 150, refund_address: quote.request.wallet_address.clone(), min_price, }, @@ -205,7 +213,7 @@ impl Swapper for ChainflipProvider { VaultSwapExtras::Bitcoin(VaultSwapBtcExtras { chain, min_output_amount: BigUint::from_bytes_le(&min_output_amount.to_le_bytes::<32>()), - retry_duration: 6, // blocks + retry_duration: 6, }) } else if from_asset.chain.chain_type() == ChainType::Solana { VaultSwapExtras::Solana(VaultSwapSolanaExtras { @@ -214,7 +222,7 @@ impl Swapper for ChainflipProvider { chain, input_amount: input_amount.to_u64().unwrap(), refund_parameters: RefundParameters { - retry_duration: 10, // blocks + retry_duration: 10, refund_address: quote.request.wallet_address.clone(), min_price, }, @@ -223,7 +231,8 @@ impl Swapper for ChainflipProvider { VaultSwapExtras::None }; - let response = broker_client + let response = self + .broker_client .encode_vault_swap( source_asset, destination_asset, @@ -237,7 +246,7 @@ impl Swapper for ChainflipProvider { match response { VaultSwapResponse::Evm(response) => { - let value: String = if from_asset.is_native() { + let value = if from_asset.is_native() { quote.request.value.clone() } else { "0".to_string() @@ -249,7 +258,7 @@ impl Swapper for ChainflipProvider { from_asset.token_id.unwrap(), response.to.clone(), U256::from_le_slice(&input_amount.to_bytes_le()), - provider.clone(), + self.rpc_provider.clone(), &from_asset.chain, ) .await?; @@ -264,52 +273,42 @@ impl Swapper for ChainflipProvider { None }; - let swap_quote_data = SwapperQuoteData { + Ok(SwapperQuoteData { to: response.to, value, data: response.calldata, approval, gas_limit, - }; - - Ok(swap_quote_data) - } - VaultSwapResponse::Bitcoin(response) => { - let swap_quote_data = SwapperQuoteData { - to: response.deposit_address, - value: quote.request.value.clone(), - data: response.nulldata_payload, - approval: None, - gas_limit: None, - }; - - Ok(swap_quote_data) + }) } + VaultSwapResponse::Bitcoin(response) => Ok(SwapperQuoteData { + to: response.deposit_address, + value: quote.request.value.clone(), + data: response.nulldata_payload, + approval: None, + gas_limit: None, + }), VaultSwapResponse::Solana(response) => { - let data = tx_builder::build_solana_tx("e.request.wallet_address, &response, provider.clone()) + let data = tx_builder::build_solana_tx("e.request.wallet_address, &response, self.rpc_provider.clone()) .await .map_err(SwapperError::TransactionError)?; - let swap_quote_data = SwapperQuoteData { + Ok(SwapperQuoteData { to: response.program_id, value: "".into(), data, approval: None, gas_limit: None, - }; - - Ok(swap_quote_data) + }) } } } - async fn get_swap_result(&self, chain: Chain, transaction_hash: &str, provider: Arc) -> Result { - let chainflip_client = ChainflipClient::new(provider.clone()); - let status = chainflip_client.get_tx_status(transaction_hash).await?; - + async fn get_swap_result(&self, chain: Chain, transaction_hash: &str) -> Result { + let status = self.chainflip_client.get_tx_status(transaction_hash).await?; let swap_status = status.swap_status(); let to_tx_hash = status.swap_egress.as_ref().and_then(|x| x.tx_ref.clone()); - Ok(SwapperSwapResult { + Ok(SwapResult { status: swap_status, from_chain: chain, from_tx_hash: transaction_hash.to_string(), @@ -327,7 +326,7 @@ mod tests { fn test_best_quote() { let json = include_str!("./test/chainflip_quotes.json"); let quotes: Vec = serde_json::from_str(json).unwrap(); - let (egress_amount, slippage_bps, eta_in_seconds, route_data) = ChainflipProvider::get_best_quote(quotes, DEFAULT_CHAINFLIP_FEE_BPS); + let (egress_amount, slippage_bps, eta_in_seconds, route_data) = get_best_quote(quotes, DEFAULT_CHAINFLIP_FEE_BPS); assert_eq!(egress_amount.to_string(), "145118751424"); assert_eq!(slippage_bps, 250); @@ -347,7 +346,7 @@ mod tests { fn test_best_boost_quote() { let json = include_str!("./test/chainflip_boost_quotes.json"); let quotes: Vec = serde_json::from_str(json).unwrap(); - let (egress_amount, slippage_bps, eta_in_seconds, route_data) = ChainflipProvider::get_best_quote(quotes, DEFAULT_CHAINFLIP_FEE_BPS); + let (egress_amount, slippage_bps, eta_in_seconds, route_data) = get_best_quote(quotes, DEFAULT_CHAINFLIP_FEE_BPS); assert_eq!(egress_amount.to_string(), "4080936927013539226"); assert_eq!(slippage_bps, 100); diff --git a/gemstone/src/swapper/chainflip/seed.rs b/crates/swapper/src/chainflip/seed.rs similarity index 100% rename from gemstone/src/swapper/chainflip/seed.rs rename to crates/swapper/src/chainflip/seed.rs diff --git a/gemstone/src/swapper/chainflip/test/chainflip_boost_quotes.json b/crates/swapper/src/chainflip/test/chainflip_boost_quotes.json similarity index 100% rename from gemstone/src/swapper/chainflip/test/chainflip_boost_quotes.json rename to crates/swapper/src/chainflip/test/chainflip_boost_quotes.json diff --git a/gemstone/src/swapper/chainflip/test/chainflip_quotes.json b/crates/swapper/src/chainflip/test/chainflip_quotes.json similarity index 100% rename from gemstone/src/swapper/chainflip/test/chainflip_quotes.json rename to crates/swapper/src/chainflip/test/chainflip_quotes.json diff --git a/gemstone/src/swapper/chainflip/test/chainflip_sol_arb_usdc_quote_data.json b/crates/swapper/src/chainflip/test/chainflip_sol_arb_usdc_quote_data.json similarity index 100% rename from gemstone/src/swapper/chainflip/test/chainflip_sol_arb_usdc_quote_data.json rename to crates/swapper/src/chainflip/test/chainflip_sol_arb_usdc_quote_data.json diff --git a/gemstone/src/swapper/chainflip/tx_builder.rs b/crates/swapper/src/chainflip/tx_builder.rs similarity index 91% rename from gemstone/src/swapper/chainflip/tx_builder.rs rename to crates/swapper/src/chainflip/tx_builder.rs index 8f1dd4a8e..4142b8c4c 100644 --- a/gemstone/src/swapper/chainflip/tx_builder.rs +++ b/crates/swapper/src/chainflip/tx_builder.rs @@ -1,5 +1,6 @@ use super::broker::SolanaVaultSwapResponse; -use crate::network::{AlienProvider, jsonrpc_client_with_chain}; +use crate::{alien::RpcProvider, client_factory::create_client_with_chain}; + use alloy_primitives::hex; use base64::Engine; use base64::engine::general_purpose::STANDARD; @@ -8,12 +9,12 @@ use primitives::Chain; use solana_primitives::{AccountMeta, InstructionBuilder, Pubkey, TransactionBuilder}; use std::{str::FromStr, sync::Arc}; -pub async fn build_solana_tx(fee_payer: &str, response: &SolanaVaultSwapResponse, provider: Arc) -> Result { +pub async fn build_solana_tx(fee_payer: &str, response: &SolanaVaultSwapResponse, provider: Arc) -> Result { let fee_payer = Pubkey::from_str(fee_payer).map_err(|_| "Invalid fee payer".to_string())?; let program_id = Pubkey::from_str(response.program_id.as_str()).map_err(|_| "Invalid program ID".to_string())?; let data = hex::decode(response.data.as_str()).map_err(|_| "Invalid data".to_string())?; - let rpc_client = jsonrpc_client_with_chain(provider, Chain::Solana); + let rpc_client = create_client_with_chain(provider, Chain::Solana); let blockhash_response: LatestBlockhash = rpc_client.request(SolanaRpc::GetLatestBlockhash).await.map_err(|e| e.to_string())?; let recent_blockhash = blockhash_response.value.blockhash; let blockhash = bs58::decode(recent_blockhash) @@ -44,8 +45,8 @@ pub async fn build_solana_tx(fee_payer: &str, response: &SolanaVaultSwapResponse mod tests { use super::*; use crate::{ - network::mock::{AlienProviderMock, MockFn}, - swapper::chainflip::broker::SolanaVaultSwapResponse, + alien::mock::{MockFn, ProviderMock}, + chainflip::broker::SolanaVaultSwapResponse, }; use gem_jsonrpc::types::JsonRpcResponse; use std::time::Duration; @@ -54,7 +55,7 @@ mod tests { async fn test_build_solana_tx_with_mocked_blockhash() -> Result<(), String> { let wallet_address = "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC"; let blockhash_b58 = "BZcyEKqjBNG5bEY6i5ev6PfPTgDSB9LwovJE1hJfJoHF".to_string(); - let mock = AlienProviderMock { + let mock = ProviderMock { response: MockFn(Box::new(move |_| { serde_json::json!({ "jsonrpc": "2.0", diff --git a/crates/swapper/src/chainlink.rs b/crates/swapper/src/chainlink.rs new file mode 100644 index 000000000..d9df74cef --- /dev/null +++ b/crates/swapper/src/chainlink.rs @@ -0,0 +1,32 @@ +use num_bigint::BigInt; +use num_traits::FromBytes; + +use crate::SwapperError; +use gem_evm::{ + chainlink::contract::{AggregatorInterface, CHAINLINK_ETH_USD_FEED}, + multicall3::{IMulticall3, create_call3, decode_call3_return}, +}; + +pub struct ChainlinkPriceFeed { + pub contract: String, +} + +impl ChainlinkPriceFeed { + pub fn new_eth_usd_feed() -> ChainlinkPriceFeed { + ChainlinkPriceFeed { + contract: CHAINLINK_ETH_USD_FEED.into(), + } + } + + pub fn latest_round_call3(&self) -> IMulticall3::Call3 { + create_call3(&self.contract, AggregatorInterface::latestRoundDataCall {}) + } + + // Price is in 8 decimals + pub fn decoded_answer(result: &IMulticall3::Result) -> Result { + let decoded = + decode_call3_return::(result).map_err(|_| SwapperError::ABIError("failed to decode answer".into()))?; + let price = BigInt::from_le_bytes(&decoded.answer.to_le_bytes::<32>()); + Ok(price) + } +} diff --git a/crates/swapper/src/client_factory.rs b/crates/swapper/src/client_factory.rs new file mode 100644 index 000000000..27f79b848 --- /dev/null +++ b/crates/swapper/src/client_factory.rs @@ -0,0 +1,30 @@ +use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::client::JsonRpcClient; +use gem_tron::rpc::{TronClient, trongrid::client::TronGridClient}; +use primitives::{Chain, EVMChain}; +use std::sync::Arc; + +use crate::{ + SwapperError, + alien::{AlienError, RpcClient, RpcProvider}, +}; + +pub fn create_client_with_chain(provider: Arc, chain: Chain) -> JsonRpcClient { + let endpoint = provider.get_endpoint(chain).expect("Failed to get endpoint for chain"); + let client = RpcClient::new(endpoint, provider); + JsonRpcClient::new(client) +} + +pub fn create_tron_client(provider: Arc) -> Result, AlienError> { + let endpoint = provider.get_endpoint(Chain::Tron)?; + let tron_rpc_client = RpcClient::new(endpoint.clone(), provider.clone()); + let trongrid_client = TronGridClient::new(RpcClient::new(endpoint, provider), String::new()); + + Ok(TronClient::new(tron_rpc_client, trongrid_client)) +} + +pub fn create_eth_client(provider: Arc, chain: Chain) -> Result, SwapperError> { + let evm_chain = EVMChain::from_chain(chain).ok_or(SwapperError::NotSupportedChain)?; + let client = create_client_with_chain(provider, chain); + Ok(EthereumClient::new(client, evm_chain)) +} diff --git a/crates/swapper/src/config.rs b/crates/swapper/src/config.rs new file mode 100644 index 000000000..d061274b3 --- /dev/null +++ b/crates/swapper/src/config.rs @@ -0,0 +1,127 @@ +use crate::{SwapperSlippage, SwapperSlippageMode}; +use primitives::Chain; + +pub static DEFAULT_SLIPPAGE_BPS: u32 = 100; +pub static DEFAULT_SWAP_FEE_BPS: u32 = 50; +pub static DEFAULT_CHAINFLIP_FEE_BPS: u32 = 45; +pub static DEFAULT_STABLE_SWAP_REFERRAL_BPS: u32 = 25; + +#[derive(Debug, Clone, PartialEq)] +pub struct Config { + pub default_slippage: SwapperSlippage, + pub permit2_expiration: u64, + pub permit2_sig_deadline: u64, + pub referral_fee: ReferralFees, + pub high_price_impact_percent: u32, +} + +#[derive(Default, Debug, Clone, PartialEq)] +pub struct ReferralFees { + pub evm: ReferralFee, + pub evm_bridge: ReferralFee, + pub solana: ReferralFee, + pub thorchain: ReferralFee, + pub sui: ReferralFee, + pub ton: ReferralFee, + pub tron: ReferralFee, +} + +#[derive(Default, Debug, Clone, PartialEq)] +pub struct ReferralFee { + pub address: String, + pub bps: u32, +} + +impl ReferralFees { + pub fn evm(evm: ReferralFee) -> ReferralFees { + ReferralFees { + evm, + evm_bridge: ReferralFee::default(), + solana: ReferralFee::default(), + thorchain: ReferralFee::default(), + sui: ReferralFee::default(), + ton: ReferralFee::default(), + tron: ReferralFee::default(), + } + } + + pub fn update_all_bps(&mut self, bps: u32) { + self.iter_mut().for_each(|fee| fee.update_bps(bps)); + } + + fn iter_mut(&mut self) -> impl Iterator { + [ + &mut self.evm, + &mut self.evm_bridge, + &mut self.solana, + &mut self.thorchain, + &mut self.sui, + &mut self.ton, + &mut self.tron, + ] + .into_iter() + } +} + +impl ReferralFee { + pub fn update_bps(&mut self, bps: u32) { + if !self.address.is_empty() || self.bps > 0 { + self.bps = bps; + } + } +} + +pub fn get_swap_config() -> Config { + Config { + default_slippage: SwapperSlippage { + bps: DEFAULT_SLIPPAGE_BPS, + mode: SwapperSlippageMode::Exact, + }, + permit2_expiration: 60 * 60 * 24 * 30, // 30 days + permit2_sig_deadline: 60 * 30, // 30 minutes + referral_fee: ReferralFees { + evm: ReferralFee { + address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), + bps: DEFAULT_SWAP_FEE_BPS, + }, + evm_bridge: ReferralFee { + address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), + bps: DEFAULT_STABLE_SWAP_REFERRAL_BPS, + }, + solana: ReferralFee { + address: "5fmLrs2GuhfDP1B51ziV5Kd1xtAr9rw1jf3aQ4ihZ2gy".into(), + bps: DEFAULT_SWAP_FEE_BPS, + }, + thorchain: ReferralFee { + address: "g1".into(), + bps: DEFAULT_SWAP_FEE_BPS, + }, + sui: ReferralFee { + address: "0x9d6b98b18fd26b5efeec68d020dcf1be7a94c2c315353779bc6b3aed44188ddf".into(), + bps: DEFAULT_SWAP_FEE_BPS, + }, + ton: ReferralFee { + address: "UQDxJKarPSp0bCta9DFgp81Mpt5hpGbuVcSxwfeza0Bin201".into(), + bps: DEFAULT_SWAP_FEE_BPS, + }, + tron: ReferralFee { + address: "TYeyZXywpA921LEtw2PF3obK4B8Jjgpp32".into(), + bps: DEFAULT_SWAP_FEE_BPS, + }, + }, + high_price_impact_percent: 10, + } +} + +pub fn get_default_slippage(chain: &Chain) -> SwapperSlippage { + match chain { + Chain::Solana => SwapperSlippage { + bps: DEFAULT_SLIPPAGE_BPS * 3, + mode: SwapperSlippageMode::Auto, + }, + _ => SwapperSlippage { + bps: DEFAULT_SLIPPAGE_BPS, + mode: SwapperSlippageMode::Exact, + }, + } +} diff --git a/gemstone/src/swapper/error.rs b/crates/swapper/src/error.rs similarity index 86% rename from gemstone/src/swapper/error.rs rename to crates/swapper/src/error.rs index 8eb6542b9..dd17f4d56 100644 --- a/gemstone/src/swapper/error.rs +++ b/crates/swapper/src/error.rs @@ -1,7 +1,9 @@ -use crate::network::{AlienError, JsonRpcError}; +use crate::alien::AlienError; +use gem_client::ClientError; +use gem_jsonrpc::types::JsonRpcError; use std::fmt::Debug; -#[derive(Debug, uniffi::Error)] +#[derive(Debug)] pub enum SwapperError { NotSupportedChain, NotSupportedAsset, @@ -58,6 +60,17 @@ impl From for SwapperError { } } +impl From for SwapperError { + fn from(err: ClientError) -> Self { + match err { + ClientError::Network(msg) => Self::NetworkError(msg), + ClientError::Timeout => Self::NetworkError("Request timed out".into()), + ClientError::Http { status, len } => Self::NetworkError(format!("HTTP error: status {}, body size: {}", status, len)), + ClientError::Serialization(msg) => Self::NetworkError(msg), + } + } +} + impl From for SwapperError { fn from(err: alloy_primitives::AddressError) -> Self { Self::InvalidAddress(err.to_string()) diff --git a/gemstone/src/swapper/eth_address.rs b/crates/swapper/src/eth_address.rs similarity index 100% rename from gemstone/src/swapper/eth_address.rs rename to crates/swapper/src/eth_address.rs diff --git a/gemstone/src/swapper/hyperliquid/mod.rs b/crates/swapper/src/hyperliquid/mod.rs similarity index 100% rename from gemstone/src/swapper/hyperliquid/mod.rs rename to crates/swapper/src/hyperliquid/mod.rs diff --git a/gemstone/src/swapper/hyperliquid/provider.rs b/crates/swapper/src/hyperliquid/provider.rs similarity index 74% rename from gemstone/src/swapper/hyperliquid/provider.rs rename to crates/swapper/src/hyperliquid/provider.rs index 8fb602850..a14023436 100644 --- a/gemstone/src/swapper/hyperliquid/provider.rs +++ b/crates/swapper/src/hyperliquid/provider.rs @@ -1,7 +1,4 @@ -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; use async_trait::async_trait; use gem_hypercore::core::{HYPE_SYSTEM_ADDRESS, HYPERCORE_HYPE_TOKEN, actions::user::spot_send::SpotSend, hypercore::transfer_to_hyper_evm_typed_data}; @@ -10,29 +7,32 @@ use number_formatter::BigNumberFormatter; use primitives::Chain; use crate::{ - network::AlienProvider, - swapper::{ - FetchQuoteData, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperProviderData, SwapperProviderType, SwapperQuote, SwapperQuoteData, - SwapperQuoteRequest, SwapperRoute, asset::HYPERCORE_HYPE, - }, + FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteData, + asset::HYPERCORE_HYPE, }; #[derive(Debug)] pub struct HyperCoreBridge { - provider: SwapperProviderType, + provider: ProviderType, } -impl Default for HyperCoreBridge { - fn default() -> Self { +impl HyperCoreBridge { + pub fn new() -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::Hyperliquid), + provider: ProviderType::new(SwapperProvider::Hyperliquid), } } } +impl Default for HyperCoreBridge { + fn default() -> Self { + Self::new() + } +} + #[async_trait] impl Swapper for HyperCoreBridge { - fn provider(&self) -> &SwapperProviderType { + fn provider(&self) -> &ProviderType { &self.provider } @@ -43,14 +43,14 @@ impl Swapper for HyperCoreBridge { ] } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, _provider: Arc) -> Result { - let quote = SwapperQuote { + async fn fetch_quote(&self, request: &QuoteRequest) -> Result { + let quote = Quote { from_value: request.value.clone(), to_value: request.value.clone(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider.clone(), slippage_bps: 0, - routes: vec![SwapperRoute { + routes: vec![Route { input: request.from_asset.asset_id(), output: request.to_asset.asset_id(), route_data: "".to_string(), @@ -64,7 +64,7 @@ impl Swapper for HyperCoreBridge { Ok(quote) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, _provider: Arc, _data: FetchQuoteData) -> Result { + async fn fetch_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/jupiter/client.rs b/crates/swapper/src/jupiter/client.rs new file mode 100644 index 000000000..516c65f18 --- /dev/null +++ b/crates/swapper/src/jupiter/client.rs @@ -0,0 +1,31 @@ +use super::model::*; +use gem_client::{CONTENT_TYPE, Client, ClientError}; +use std::collections::HashMap; + +#[derive(Clone, Debug)] +pub struct JupiterClient +where + C: Client + Clone, +{ + client: C, +} + +impl JupiterClient +where + C: Client + Clone, +{ + pub fn new(client: C) -> Self { + Self { client } + } + + pub async fn get_swap_quote(&self, request: QuoteRequest) -> Result { + let query_string = serde_urlencoded::to_string(&request).map_err(|e| ClientError::Serialization(e.to_string()))?; + let path = format!("/swap/v1/quote?{}", query_string); + self.client.get(&path).await + } + + pub async fn get_swap_quote_data(&self, request: &QuoteDataRequest) -> Result { + let headers = HashMap::from([(CONTENT_TYPE.to_string(), "application/json".into())]); + self.client.post("/swap/v1/swap", request, Some(headers)).await + } +} diff --git a/crates/swapper/src/jupiter/default.rs b/crates/swapper/src/jupiter/default.rs new file mode 100644 index 000000000..c6477083e --- /dev/null +++ b/crates/swapper/src/jupiter/default.rs @@ -0,0 +1,16 @@ +use super::{client::JupiterClient, provider::JUPITER_API_URL, provider::Jupiter}; +use crate::alien::{RpcClient, RpcProvider}; +use gem_jsonrpc::client::JsonRpcClient; +use primitives::Chain; +use std::sync::Arc; + +impl Jupiter { + pub fn new(provider: Arc) -> Self { + let http_client = JupiterClient::new(RpcClient::new(JUPITER_API_URL.into(), provider.clone())); + let solana_endpoint = provider + .get_endpoint(Chain::Solana) + .expect("Failed to get Solana endpoint for Jupiter provider"); + let rpc_client = JsonRpcClient::new(RpcClient::new(solana_endpoint, provider)); + Self::with_clients(http_client, rpc_client) + } +} diff --git a/gemstone/src/swapper/jupiter/mod.rs b/crates/swapper/src/jupiter/mod.rs similarity index 92% rename from gemstone/src/swapper/jupiter/mod.rs rename to crates/swapper/src/jupiter/mod.rs index a30598a6e..edcfd560e 100644 --- a/gemstone/src/swapper/jupiter/mod.rs +++ b/crates/swapper/src/jupiter/mod.rs @@ -1,4 +1,5 @@ mod client; +mod default; mod model; mod provider; mod token_account; diff --git a/gemstone/src/swapper/jupiter/model.rs b/crates/swapper/src/jupiter/model.rs similarity index 100% rename from gemstone/src/swapper/jupiter/model.rs rename to crates/swapper/src/jupiter/model.rs diff --git a/gemstone/src/swapper/jupiter/provider.rs b/crates/swapper/src/jupiter/provider.rs similarity index 71% rename from gemstone/src/swapper/jupiter/provider.rs rename to crates/swapper/src/jupiter/provider.rs index 4ee8af660..9f7ff4981 100644 --- a/gemstone/src/swapper/jupiter/provider.rs +++ b/crates/swapper/src/jupiter/provider.rs @@ -1,11 +1,16 @@ -use super::{PROGRAM_ADDRESS, client::JupiterClient, model::*}; +use super::{ + PROGRAM_ADDRESS, + client::JupiterClient, + model::{DynamicSlippage, QuoteDataRequest, QuoteRequest as JupiterRequest, QuoteResponse}, +}; use crate::{ - network::{JsonRpcResult, jsonrpc_client_with_chain}, - swapper::{Swapper, *}, + FetchQuoteData, Options, ProviderData, ProviderType, Quote, QuoteRequest, Route, Swapper, SwapperChainAsset, SwapperError, SwapperMode, SwapperProvider, + SwapperQuoteData, SwapperSlippageMode, }; - use alloy_primitives::U256; use async_trait::async_trait; +use gem_client::Client; +use gem_jsonrpc::{client::JsonRpcClient, types::JsonRpcResult}; use gem_solana::{ SolanaRpc, TOKEN_PROGRAM, USDC_TOKEN_MINT, USDS_TOKEN_MINT, USDT_TOKEN_MINT, WSOL_TOKEN_ADDRESS, get_pubkey_by_str, models::{AccountData, ValueResult}, @@ -13,24 +18,36 @@ use gem_solana::{ use primitives::{AssetId, Chain}; use std::collections::HashSet; +pub(crate) const JUPITER_API_URL: &str = "https://lite-api.jup.ag"; + #[derive(Debug)] -pub struct Jupiter { - pub provider: SwapperProviderType, +pub struct Jupiter +where + C: Client + Clone + Send + Sync + 'static, + R: Client + Clone + Send + Sync + 'static, +{ + pub provider: ProviderType, pub fee_mints: HashSet<&'static str>, + http_client: JupiterClient, + rpc_client: JsonRpcClient, } -impl Default for Jupiter { - fn default() -> Self { +impl Jupiter +where + C: Client + Clone + Send + Sync + 'static, + R: Client + Clone + Send + Sync + 'static, +{ + pub fn with_clients(http_client: JupiterClient, rpc_client: JsonRpcClient) -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::Jupiter), + provider: ProviderType::new(SwapperProvider::Jupiter), fee_mints: HashSet::from([USDC_TOKEN_MINT, USDT_TOKEN_MINT, USDS_TOKEN_MINT, WSOL_TOKEN_ADDRESS]), + http_client, + rpc_client, } } -} -impl Jupiter { - pub fn get_endpoint(&self) -> String { - "https://lite-api.jup.ag".into() + pub fn api_url() -> &'static str { + JUPITER_API_URL } pub fn get_asset_address(&self, asset_id: &str) -> Result { @@ -39,7 +56,7 @@ impl Jupiter { .ok_or(SwapperError::InvalidAddress(asset_id.to_string())) } - pub fn get_fee_mint(&self, mode: &SwapperMode, input: &str, output: &str) -> String { + fn get_fee_mint(&self, mode: &SwapperMode, input: &str, output: &str) -> String { match mode { SwapperMode::ExactIn => { if self.fee_mints.contains(output) { @@ -51,7 +68,7 @@ impl Jupiter { } } - pub fn get_fee_token_account(&self, options: &SwapperOptions, mint: &str, token_program: &str) -> Option { + fn get_fee_token_account(&self, options: &Options, mint: &str, token_program: &str) -> Option { if let Some(fee) = &options.fee { let fee_account = super::token_account::get_token_account(&fee.solana.address, mint, token_program); return Some(fee_account); @@ -59,11 +76,10 @@ impl Jupiter { None } - pub async fn fetch_token_program(&self, mint: &str, provider: Arc) -> Result { + async fn fetch_token_program(&self, mint: &str) -> Result { let rpc_call = SolanaRpc::GetAccountInfo(mint.to_string()); - let client = jsonrpc_client_with_chain(provider.clone(), Chain::Solana); let rpc_result: JsonRpcResult>> = - client.call_with_cache(&rpc_call, Some(u64::MAX)).await.map_err(SwapperError::from)?; + self.rpc_client.call_with_cache(&rpc_call, Some(u64::MAX)).await.map_err(SwapperError::from)?; let value = rpc_result.take()?; value @@ -72,20 +88,13 @@ impl Jupiter { .ok_or(SwapperError::NetworkError("fetch_token_program error".to_string())) } - pub async fn fetch_fee_account( - &self, - mode: &SwapperMode, - options: &SwapperOptions, - input_mint: &str, - output_mint: &str, - provider: Arc, - ) -> Result { + async fn fetch_fee_account(&self, mode: &SwapperMode, options: &Options, input_mint: &str, output_mint: &str) -> Result { let fee_mint = self.get_fee_mint(mode, input_mint, output_mint); // if fee_mint is in preset, no need to fetch token program let token_program = if self.fee_mints.contains(fee_mint.as_str()) { - return Ok(self.get_fee_token_account(options, fee_mint.as_str(), TOKEN_PROGRAM).unwrap()); + return Ok(self.get_fee_token_account(options, fee_mint.as_str(), TOKEN_PROGRAM).unwrap_or_default()); } else { - self.fetch_token_program(&fee_mint, provider.clone()).await? + self.fetch_token_program(&fee_mint).await? }; let mut fee_account = self.get_fee_token_account(options, &fee_mint, &token_program).unwrap_or_default(); @@ -95,8 +104,7 @@ impl Jupiter { // check fee token account exists, if not, set fee_account to empty string let rpc_call = SolanaRpc::GetAccountInfo(fee_account.clone()); - let client = jsonrpc_client_with_chain(provider.clone(), Chain::Solana); - let rpc_result: JsonRpcResult>> = client.call_with_cache(&rpc_call, None).await.map_err(SwapperError::from)?; + let rpc_result: JsonRpcResult>> = self.rpc_client.call_with_cache(&rpc_call, None).await.map_err(SwapperError::from)?; if matches!(rpc_result, JsonRpcResult::Error(_)) || matches!(rpc_result, JsonRpcResult::Value(ref resp) if resp.result.value.is_none()) { fee_account = String::from(""); } @@ -105,8 +113,12 @@ impl Jupiter { } #[async_trait] -impl Swapper for Jupiter { - fn provider(&self) -> &SwapperProviderType { +impl Swapper for Jupiter +where + C: Client + Clone + Send + Sync + 'static, + R: Client + Clone + Send + Sync + 'static, +{ + fn provider(&self) -> &ProviderType { &self.provider } @@ -114,7 +126,7 @@ impl Swapper for Jupiter { vec![SwapperChainAsset::All(Chain::Solana)] } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result { + async fn fetch_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(); @@ -126,7 +138,7 @@ impl Swapper for Jupiter { SwapperSlippageMode::Exact => false, }; - let quote_request = QuoteRequest { + let quote_request = JupiterRequest { input_mint: input_mint.clone(), output_mint: output_mint.clone(), amount: request.value.clone(), @@ -135,20 +147,19 @@ impl Swapper for Jupiter { auto_slippage, max_auto_slippage_bps: slippage_bps, }; - let client = JupiterClient::new(self.get_endpoint(), provider.clone()); - let swap_quote = client.get_swap_quote(quote_request).await?; + let swap_quote = self.http_client.get_swap_quote(quote_request).await?; let computed_auto_slippage = swap_quote.computed_auto_slippage.unwrap_or(swap_quote.slippage_bps); // Updated docs: https://dev.jup.ag/docs/api/swap-api/quote // The value includes platform fees and DEX fees, excluding slippage. let out_amount: U256 = swap_quote.out_amount.parse().map_err(SwapperError::from)?; - let quote = SwapperQuote { + let quote = Quote { from_value: request.value.clone(), to_value: out_amount.to_string(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider().clone(), - routes: vec![SwapperRoute { + routes: vec![Route { input: AssetId::from(Chain::Solana, Some(input_mint)), output: AssetId::from(Chain::Solana, Some(output_mint)), route_data: serde_json::to_string(&swap_quote).unwrap_or_default(), @@ -162,7 +173,7 @@ impl Swapper for Jupiter { Ok(quote) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, provider: Arc, _data: FetchQuoteData) -> Result { + async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { if quote.data.routes.is_empty() { return Err(SwapperError::InvalidRoute); } @@ -172,7 +183,7 @@ impl Swapper for Jupiter { let quote_response: QuoteResponse = serde_json::from_str(&route.route_data).map_err(|_| SwapperError::InvalidRoute)?; let fee_account = self - .fetch_fee_account("e.request.mode, "e.request.options, &input_mint, &output_mint, provider.clone()) + .fetch_fee_account("e.request.mode, "e.request.options, &input_mint, &output_mint) .await?; let dynamic_slippage = match quote.request.options.slippage.mode { @@ -190,8 +201,7 @@ impl Swapper for Jupiter { dynamic_slippage, }; - let client = JupiterClient::new(self.get_endpoint(), provider); - let quote_data = client.get_swap_quote_data(request).await?; + let quote_data = self.http_client.get_swap_quote_data(&request).await?; if let Some(simulation_error) = quote_data.simulation_error { return Err(SwapperError::TransactionError(simulation_error.error)); @@ -211,24 +221,22 @@ impl Swapper for Jupiter { #[cfg(all(test, feature = "swap_integration_tests"))] mod swap_integration_tests { use super::*; - use crate::{ - network::alien_provider::NativeProvider, - swapper::{SwapperMode, models::SwapperOptions, remote_models::SwapperQuoteAsset}, - }; + use crate::{SwapperMode, SwapperQuoteAsset, alien::reqwest_provider::NativeProvider, models::Options}; use primitives::AssetId; + use std::sync::Arc; #[tokio::test] async fn test_jupiter_provider_fetch_quote() -> Result<(), SwapperError> { - let provider = Jupiter::default(); - let network_provider = Arc::new(NativeProvider::default()); + let rpc_provider = Arc::new(NativeProvider::default()); + let provider = Jupiter::new(rpc_provider); - let options = SwapperOptions { + let options = Options { slippage: 100.into(), fee: None, preferred_providers: vec![], }; - let request = SwapperQuoteRequest { + let request = QuoteRequest { from_asset: SwapperQuoteAsset::from(AssetId::from_chain(Chain::Solana)), to_asset: SwapperQuoteAsset::from(AssetId::from(Chain::Solana, Some(USDC_TOKEN_MINT.to_string()))), wallet_address: "7g2rVN8fAAQdPh1mkajpvELqYa3gWvFXJsBLnKfEQfqy".to_string(), @@ -238,7 +246,7 @@ mod swap_integration_tests { options, }; - let quote = provider.fetch_quote(&request, network_provider).await?; + let quote = provider.fetch_quote(&request).await?; assert_eq!(quote.from_value, request.value); assert!(quote.to_value.parse::().unwrap() > 0); diff --git a/gemstone/src/swapper/jupiter/token_account.rs b/crates/swapper/src/jupiter/token_account.rs similarity index 100% rename from gemstone/src/swapper/jupiter/token_account.rs rename to crates/swapper/src/jupiter/token_account.rs diff --git a/crates/swapper/src/lib.rs b/crates/swapper/src/lib.rs new file mode 100644 index 000000000..909834be0 --- /dev/null +++ b/crates/swapper/src/lib.rs @@ -0,0 +1,42 @@ +mod alien; +mod approval; +mod chainlink; +mod eth_address; +mod swapper_trait; + +#[cfg(test)] +pub mod testkit; + +pub mod across; +pub mod asset; +pub mod cetus; +pub mod chainflip; +pub mod client_factory; +pub mod config; +pub mod error; +pub mod hyperliquid; +pub mod jupiter; +pub mod models; +pub mod pancakeswap_aptos; +pub mod permit2_data; +pub mod proxy; +pub mod slippage; +pub mod swapper; +pub mod thorchain; +pub mod uniswap; + +#[cfg(feature = "reqwest_provider")] +pub use alien::reqwest_provider::NativeProvider; +pub use alien::{AlienError, HttpMethod, RpcClient, RpcProvider, Target}; +pub use error::SwapperError; +pub use models::*; +pub(crate) use swapper_trait::Swapper; + +pub type SwapperProvider = primitives::SwapProvider; +pub type SwapperProviderMode = primitives::swap::SwapProviderMode; +pub type SwapperQuoteAsset = primitives::swap::QuoteAsset; +pub type SwapperMode = primitives::swap::SwapMode; +pub type SwapperSlippage = primitives::swap::Slippage; +pub type SwapperSlippageMode = primitives::swap::SlippageMode; +pub type SwapperQuoteData = primitives::swap::SwapQuoteData; +pub type SwapperSwapStatus = primitives::swap::SwapStatus; diff --git a/gemstone/src/swapper/models.rs b/crates/swapper/src/models.rs similarity index 64% rename from gemstone/src/swapper/models.rs rename to crates/swapper/src/models.rs index a3c72ec64..9045a460a 100644 --- a/gemstone/src/swapper/models.rs +++ b/crates/swapper/src/models.rs @@ -1,46 +1,20 @@ use super::permit2_data::Permit2Data; -use crate::config::swap_config::{DEFAULT_SLIPPAGE_BPS, SwapReferralFees}; -use crate::models::GemApprovalData; -use primitives::{AssetId, Chain}; +use crate::{ + SwapperMode, SwapperProvider, SwapperProviderMode, SwapperQuoteAsset, SwapperSlippage, SwapperSwapStatus, + config::{DEFAULT_SLIPPAGE_BPS, ReferralFees}, +}; +use primitives::{AssetId, Chain, swap::ApprovalData}; use std::fmt::Debug; -use std::str::FromStr; -use super::remote_models::*; - -#[derive(Debug, Clone, PartialEq, uniffi::Object)] -pub struct SwapProviderConfig(SwapperProviderType); - -impl SwapProviderConfig { - pub fn id(&self) -> SwapperProvider { - self.0.id - } -} - -#[uniffi::export] -impl SwapProviderConfig { - #[uniffi::constructor] - pub fn new(id: SwapperProvider) -> Self { - Self(SwapperProviderType::new(id)) - } - #[uniffi::constructor] - pub fn from_string(id: String) -> Self { - let id = SwapperProvider::from_str(&id).unwrap(); - Self(SwapperProviderType::new(id)) - } - pub fn inner(&self) -> SwapperProviderType { - self.0.clone() - } -} - -#[derive(Debug, Clone, PartialEq, uniffi::Record)] -pub struct SwapperProviderType { +#[derive(Debug, Clone, PartialEq)] +pub struct ProviderType { pub id: SwapperProvider, pub name: String, pub protocol: String, pub protocol_id: String, } -impl SwapperProviderType { +impl ProviderType { pub fn new(id: SwapperProvider) -> Self { Self { id, @@ -73,25 +47,25 @@ impl SwapperProviderType { } } -#[derive(Debug, Clone, uniffi::Record)] -pub struct SwapperQuoteRequest { +#[derive(Debug, Clone, PartialEq)] +pub struct QuoteRequest { pub from_asset: SwapperQuoteAsset, pub to_asset: SwapperQuoteAsset, pub wallet_address: String, pub destination_address: String, pub value: String, pub mode: SwapperMode, - pub options: SwapperOptions, + pub options: Options, } -#[derive(Debug, Clone, uniffi::Record)] -pub struct SwapperOptions { +#[derive(Debug, Clone, PartialEq)] +pub struct Options { pub slippage: SwapperSlippage, - pub fee: Option, + pub fee: Option, pub preferred_providers: Vec, } -impl Default for SwapperOptions { +impl Default for Options { fn default() -> Self { Self { slippage: DEFAULT_SLIPPAGE_BPS.into(), @@ -101,24 +75,24 @@ impl Default for SwapperOptions { } } -#[derive(Debug, Clone, uniffi::Record)] -pub struct SwapperQuote { +#[derive(Debug, Clone, PartialEq)] +pub struct Quote { pub from_value: String, pub to_value: String, - pub data: SwapperProviderData, - pub request: SwapperQuoteRequest, + pub data: ProviderData, + pub request: QuoteRequest, pub eta_in_seconds: Option, } -#[derive(Debug, Clone, PartialEq, uniffi::Enum)] +#[derive(Debug, Clone, PartialEq)] pub enum ApprovalType { - Approve(GemApprovalData), + Approve(ApprovalData), Permit2(Permit2ApprovalData), None, } impl ApprovalType { - pub fn approval_data(&self) -> Option { + pub fn approval_data(&self) -> Option { match self { Self::Approve(data) => Some(data.clone()), _ => None, @@ -132,7 +106,7 @@ impl ApprovalType { } } -#[derive(Debug, Clone, PartialEq, uniffi::Record)] +#[derive(Debug, Clone, PartialEq)] pub struct Permit2ApprovalData { pub token: String, pub spender: String, @@ -141,22 +115,22 @@ pub struct Permit2ApprovalData { pub permit2_nonce: u64, } -#[derive(Debug, Clone, uniffi::Record)] -pub struct SwapperProviderData { - pub provider: SwapperProviderType, +#[derive(Debug, Clone, PartialEq)] +pub struct ProviderData { + pub provider: ProviderType, pub slippage_bps: u32, - pub routes: Vec, + pub routes: Vec, } -#[derive(Debug, Clone, uniffi::Record)] -pub struct SwapperRoute { +#[derive(Debug, Clone, PartialEq)] +pub struct Route { pub input: AssetId, pub output: AssetId, pub route_data: String, pub gas_limit: Option, } -#[derive(Debug, Clone, uniffi::Enum)] +#[derive(Debug, Clone, PartialEq)] pub enum FetchQuoteData { Permit2(Permit2Data), EstimateGas, @@ -172,7 +146,7 @@ impl FetchQuoteData { } } -#[derive(Debug, Clone, uniffi::Enum, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum SwapperChainAsset { All(Chain), Assets(Chain, Vec), @@ -187,14 +161,14 @@ impl SwapperChainAsset { } } -#[derive(Debug, Clone, uniffi::Record, PartialEq)] -pub struct SwapperAssetList { +#[derive(Debug, Clone, PartialEq)] +pub struct AssetList { pub chains: Vec, pub asset_ids: Vec, } -#[derive(Debug, Clone, uniffi::Record)] -pub struct SwapperSwapResult { +#[derive(Debug, Clone, PartialEq)] +pub struct SwapResult { pub status: SwapperSwapStatus, pub from_chain: Chain, pub from_tx_hash: String, diff --git a/crates/swapper/src/pancakeswap_aptos/client.rs b/crates/swapper/src/pancakeswap_aptos/client.rs new file mode 100644 index 000000000..34d51a19b --- /dev/null +++ b/crates/swapper/src/pancakeswap_aptos/client.rs @@ -0,0 +1,62 @@ +use crate::SwapperError; +use gem_aptos::models::Resource; +use gem_client::{Client, ClientError}; +use num_bigint::BigUint; +use std::fmt::Debug; +use std::str::FromStr; + +use super::model::{PANCAKE_SWAP_APTOS_ADDRESS, TokenPairReserve}; + +#[derive(Clone, Debug)] +pub struct PancakeSwapAptosClient +where + C: Client + Clone + Debug, +{ + client: C, +} + +impl PancakeSwapAptosClient +where + C: Client + Clone + Debug, +{ + pub fn new(client: C) -> Self { + Self { client } + } + + pub async fn get_quote(&self, from_asset: &str, to_asset: &str, value: &str, slippage_bps: u32) -> Result { + let (asset1, asset2) = sort_assets(from_asset, to_asset); + let address = PANCAKE_SWAP_APTOS_ADDRESS; + let resource = format!("{address}::swap::TokenPairReserve<{asset1}, {asset2}>"); + let path = format!("/v1/accounts/{address}/resource/{resource}"); + + let reserve: Resource = self.client.get(&path).await.map_err(map_client_error)?; + + let reserve_x = BigUint::from_str(reserve.data.reserve_x.as_str()).unwrap_or_default(); + let reserve_y = BigUint::from_str(reserve.data.reserve_y.as_str()).unwrap_or_default(); + + let reserve_in = if asset1 == from_asset { reserve_x.clone() } else { reserve_y.clone() }; + let reserve_out = if asset1 == from_asset { reserve_y.clone() } else { reserve_x.clone() }; + let amount_in = BigUint::from_str(value).unwrap_or_default(); + + let output = calculate_swap_output(reserve_in, reserve_out, amount_in, slippage_bps); + + Ok(output.to_string()) + } +} + +fn calculate_swap_output(reserve_in: BigUint, reserve_out: BigUint, amount_in: BigUint, fee_bps: u32) -> BigUint { + let bps_base = BigUint::from(10_000u32); + let effective_fee = &bps_base - BigUint::from(fee_bps); + let effective_amount_in = &amount_in * effective_fee / &bps_base; + let numerator = &reserve_out * &effective_amount_in; + let denominator = &reserve_in + &effective_amount_in; + numerator / denominator +} + +fn sort_assets(asset1: T, asset2: T) -> (T, T) { + if asset1 <= asset2 { (asset1, asset2) } else { (asset2, asset1) } +} + +fn map_client_error(err: ClientError) -> SwapperError { + SwapperError::from(err) +} diff --git a/crates/swapper/src/pancakeswap_aptos/default.rs b/crates/swapper/src/pancakeswap_aptos/default.rs new file mode 100644 index 000000000..129d9bd89 --- /dev/null +++ b/crates/swapper/src/pancakeswap_aptos/default.rs @@ -0,0 +1,12 @@ +use super::{PancakeSwapAptos, client::PancakeSwapAptosClient}; +use crate::alien::{RpcClient, RpcProvider}; +use primitives::Chain; +use std::sync::Arc; + +impl PancakeSwapAptos { + pub fn new(rpc_provider: Arc) -> Self { + let endpoint = rpc_provider.get_endpoint(Chain::Aptos).expect("Failed to get Aptos endpoint"); + let client = PancakeSwapAptosClient::new(RpcClient::new(endpoint, rpc_provider)); + Self::with_client(client) + } +} diff --git a/gemstone/src/swapper/pancakeswap_aptos/mod.rs b/crates/swapper/src/pancakeswap_aptos/mod.rs similarity index 73% rename from gemstone/src/swapper/pancakeswap_aptos/mod.rs rename to crates/swapper/src/pancakeswap_aptos/mod.rs index 03be13081..12c77506a 100644 --- a/gemstone/src/swapper/pancakeswap_aptos/mod.rs +++ b/crates/swapper/src/pancakeswap_aptos/mod.rs @@ -1,33 +1,38 @@ -use std::sync::Arc; - mod client; +mod default; mod model; use super::{ - FetchQuoteData, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperProviderData, SwapperProviderType, SwapperQuote, SwapperQuoteData, - SwapperQuoteRequest, SwapperRoute, + FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteData, }; -use crate::network::AlienProvider; use async_trait::async_trait; use client::PancakeSwapAptosClient; use gem_aptos::{APTOS_NATIVE_COIN, TransactionPayload}; +use gem_client::Client; use model::{PANCAKE_SWAP_APTOS_ADDRESS, RouteData}; use primitives::{AssetId, Chain}; +use std::fmt::Debug; #[derive(Debug)] -pub struct PancakeSwapAptos { - pub provider: SwapperProviderType, +pub struct PancakeSwapAptos +where + C: Client + Clone + Debug, +{ + pub provider: ProviderType, + client: PancakeSwapAptosClient, } -impl Default for PancakeSwapAptos { - fn default() -> Self { +impl PancakeSwapAptos +where + C: Client + Clone + Debug, +{ + pub fn with_client(client: PancakeSwapAptosClient) -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::PancakeswapAptosV2), + provider: ProviderType::new(SwapperProvider::PancakeswapAptosV2), + client, } } -} -impl PancakeSwapAptos { fn to_asset(&self, asset_id: AssetId) -> String { if let Some(token_id) = asset_id.token_id { return token_id; @@ -52,8 +57,11 @@ impl PancakeSwapAptos { } #[async_trait] -impl Swapper for PancakeSwapAptos { - fn provider(&self) -> &SwapperProviderType { +impl Swapper for PancakeSwapAptos +where + C: Client + Clone + Debug + Send + Sync + 'static, +{ + fn provider(&self) -> &ProviderType { &self.provider } @@ -61,17 +69,14 @@ impl Swapper for PancakeSwapAptos { vec![SwapperChainAsset::All(Chain::Aptos)] } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result { - let endpoint: String = provider.get_endpoint(Chain::Aptos).unwrap(); - let client = PancakeSwapAptosClient::new(provider); - + async fn fetch_quote(&self, request: &QuoteRequest) -> Result { let from_internal_asset = self.to_asset(request.from_asset.asset_id()); let to_internal_asset = self.to_asset(request.to_asset.asset_id()); let fee_bps = 0; // TODO: implement fees - let quote_value = client + let quote_value = self + .client .get_quote( - endpoint.as_str(), from_internal_asset.as_str(), to_internal_asset.as_str(), request.value.to_string().as_str(), @@ -85,12 +90,12 @@ impl Swapper for PancakeSwapAptos { }; let route_data = serde_json::to_string(&route_data).unwrap(); - let quote = SwapperQuote { + let quote = Quote { from_value: request.value.clone(), to_value: quote_value.clone(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider().clone(), - routes: vec![SwapperRoute { + routes: vec![Route { input: request.from_asset.asset_id(), output: request.to_asset.asset_id(), route_data, @@ -105,7 +110,7 @@ impl Swapper for PancakeSwapAptos { Ok(quote) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, _provider: Arc, _data: FetchQuoteData) -> Result { + async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let routes = quote.data.clone().routes; let route_data: RouteData = serde_json::from_str(&routes.first().unwrap().route_data).map_err(|_| SwapperError::InvalidRoute)?; diff --git a/gemstone/src/swapper/pancakeswap_aptos/model.rs b/crates/swapper/src/pancakeswap_aptos/model.rs similarity index 100% rename from gemstone/src/swapper/pancakeswap_aptos/model.rs rename to crates/swapper/src/pancakeswap_aptos/model.rs diff --git a/gemstone/src/swapper/permit2_data.rs b/crates/swapper/src/permit2_data.rs similarity index 94% rename from gemstone/src/swapper/permit2_data.rs rename to crates/swapper/src/permit2_data.rs index 5c8342499..580bb4f9d 100644 --- a/gemstone/src/swapper/permit2_data.rs +++ b/crates/swapper/src/permit2_data.rs @@ -1,4 +1,5 @@ -use crate::swapper::SwapperError; +use crate::SwapperError; + use alloy_primitives::{ Bytes, U256, aliases::{U48, U160}, @@ -16,7 +17,7 @@ use gem_evm::{ }; use primitives::Chain; -#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Permit2Detail { pub token: String, pub amount: String, @@ -26,7 +27,7 @@ pub struct Permit2Detail { pub nonce: u64, } -#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PermitSingle { pub details: Permit2Detail, pub spender: String, @@ -49,7 +50,7 @@ impl From for IAllowanceTransfer::PermitSingle { } } -#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Permit2Data { pub permit_single: PermitSingle, pub signature: Vec, @@ -81,7 +82,6 @@ where serializer.serialize_str(&value.to_string()) } -#[uniffi::export] pub fn permit2_data_to_eip712_json(chain: Chain, data: PermitSingle, contract: &str) -> Result { let chain_id = chain.network_id(); let message = Permit2Message { diff --git a/crates/swapper/src/proxy/client.rs b/crates/swapper/src/proxy/client.rs new file mode 100644 index 000000000..76ef72287 --- /dev/null +++ b/crates/swapper/src/proxy/client.rs @@ -0,0 +1,49 @@ +use crate::SwapperError; +use gem_client::{Client, ClientError}; +use primitives::swap::{ProxyQuote, ProxyQuoteRequest, SwapQuoteData}; +use serde::Deserialize; +use std::fmt::Debug; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum ProxyResult { + Ok(T), + Err { error: String }, +} + +#[derive(Clone, Debug)] +pub struct ProxyClient +where + C: Client + Clone + Debug, +{ + client: C, +} + +impl ProxyClient +where + C: Client + Clone + Debug, +{ + pub fn new(client: C) -> Self { + Self { client } + } + + pub async fn get_quote(&self, request: ProxyQuoteRequest) -> Result { + let response: ProxyResult = self.client.post("/quote", &request, None).await.map_err(map_client_error)?; + match response { + ProxyResult::Ok(q) => Ok(q), + ProxyResult::Err { error } => Err(SwapperError::ComputeQuoteError(error)), + } + } + + pub async fn get_quote_data(&self, quote: ProxyQuote) -> Result { + let response: ProxyResult = self.client.post("/quote_data", "e, None).await.map_err(map_client_error)?; + match response { + ProxyResult::Ok(qd) => Ok(qd), + ProxyResult::Err { error } => Err(SwapperError::TransactionError(error)), + } + } +} + +fn map_client_error(err: ClientError) -> SwapperError { + SwapperError::from(err) +} diff --git a/gemstone/src/swapper/proxy/mayan/explorer.rs b/crates/swapper/src/proxy/mayan/explorer.rs similarity index 72% rename from gemstone/src/swapper/proxy/mayan/explorer.rs rename to crates/swapper/src/proxy/mayan/explorer.rs index 6c56c192d..8ad3670a3 100644 --- a/gemstone/src/swapper/proxy/mayan/explorer.rs +++ b/crates/swapper/src/proxy/mayan/explorer.rs @@ -1,22 +1,22 @@ use super::model::MayanTransactionResult; use crate::{ - network::{AlienProvider, AlienTarget}, - swapper::SwapperError, + SwapperError, + alien::{RpcProvider, Target}, }; use std::sync::Arc; pub struct MayanExplorer { - provider: Arc, + provider: Arc, } impl MayanExplorer { - pub fn new(provider: Arc) -> Self { + pub fn new(provider: Arc) -> Self { Self { provider } } pub async fn get_transaction_status(&self, tx_hash: &str) -> Result { let url = format!("https://explorer-api.mayan.finance/v3/swap/trx/{tx_hash}"); - let target = AlienTarget::get(&url); + let target = Target::get(&url); let response = self.provider.request(target).await?; let result: MayanTransactionResult = serde_json::from_slice(&response).map_err(SwapperError::from)?; diff --git a/gemstone/src/swapper/proxy/mayan/mod.rs b/crates/swapper/src/proxy/mayan/mod.rs similarity index 100% rename from gemstone/src/swapper/proxy/mayan/mod.rs rename to crates/swapper/src/proxy/mayan/mod.rs diff --git a/gemstone/src/swapper/proxy/mayan/model.rs b/crates/swapper/src/proxy/mayan/model.rs similarity index 100% rename from gemstone/src/swapper/proxy/mayan/model.rs rename to crates/swapper/src/proxy/mayan/model.rs diff --git a/gemstone/src/swapper/proxy/mod.rs b/crates/swapper/src/proxy/mod.rs similarity index 100% rename from gemstone/src/swapper/proxy/mod.rs rename to crates/swapper/src/proxy/mod.rs diff --git a/gemstone/src/swapper/proxy/provider.rs b/crates/swapper/src/proxy/provider.rs similarity index 55% rename from gemstone/src/swapper/proxy/provider.rs rename to crates/swapper/src/proxy/provider.rs index 8bdb3964e..13cf62229 100644 --- a/gemstone/src/swapper/proxy/provider.rs +++ b/crates/swapper/src/proxy/provider.rs @@ -1,7 +1,6 @@ use alloy_primitives::U256; use async_trait::async_trait; -use std::str::FromStr; -use std::sync::Arc; +use std::{fmt::Debug, str::FromStr, sync::Arc}; use super::{ client::ProxyClient, @@ -9,40 +8,49 @@ use super::{ symbiosis::model::SymbiosisTransactionData, }; use crate::{ - config::swap_config::DEFAULT_SWAP_FEE_BPS, - models::GemApprovalData, - network::AlienProvider, - swapper::{ - FetchQuoteData, Swapper, SwapperError, SwapperProvider, SwapperProviderData, SwapperProviderType, SwapperQuote, SwapperQuoteData, SwapperQuoteRequest, - SwapperRoute, SwapperSwapResult, - approval::{evm::check_approval_erc20, tron::check_approval_tron}, - models::{ApprovalType, SwapperChainAsset}, - remote_models::SwapperProviderMode, - }, - tron::client::TronClient, + FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, SwapResult, Swapper, SwapperError, SwapperProvider, SwapperProviderMode, + SwapperQuoteData, + alien::{RpcClient, RpcProvider}, + approval::{evm::check_approval_erc20, tron::check_approval_tron}, + asset::*, + client_factory::create_tron_client, + config::DEFAULT_SWAP_FEE_BPS, + models::{ApprovalType, SwapperChainAsset}, }; +use gem_client::Client; use primitives::{ AssetId, Chain, ChainType, - swap::{ProxyQuote, ProxyQuoteRequest, SwapQuoteData}, + swap::{ApprovalData, ProxyQuote, ProxyQuoteRequest, SwapQuoteData}, }; pub const PROVIDER_API_URL: &str = "https://api.gemwallet.com/swapper"; const DEFAULT_GAS_LIMIT: u64 = 500000; #[derive(Debug)] -pub struct ProxyProvider { - pub provider: SwapperProviderType, - pub url: String, +pub struct ProxyProvider +where + C: Client + Clone + Send + Sync + Debug + 'static, +{ + pub provider: ProviderType, pub assets: Vec, + client: ProxyClient, + pub(crate) rpc_provider: Arc, } -impl ProxyProvider { - pub async fn check_approval( - &self, - quote: &SwapperQuote, - quote_data: &SwapQuoteData, - provider: Arc, - ) -> Result<(Option, Option), SwapperError> { +impl ProxyProvider +where + C: Client + Clone + Send + Sync + Debug + 'static, +{ + fn new_with_client(provider: SwapperProvider, client: ProxyClient, assets: Vec, rpc_provider: Arc) -> Self { + Self { + provider: ProviderType::new(provider), + assets, + client, + rpc_provider, + } + } + + pub async fn check_approval(&self, quote: &Quote, quote_data: &SwapQuoteData) -> Result<(Option, Option), SwapperError> { let request = "e.request; let from_asset = request.from_asset.asset_id(); @@ -57,7 +65,6 @@ impl ProxyProvider { token, quote_data.to.clone(), U256::from_str("e.from_value).map_err(SwapperError::from)?, - provider, &from_asset.chain, ) .await @@ -65,15 +72,8 @@ impl ProxyProvider { } ChainType::Tron => { let amount = U256::from_str("e.from_value).map_err(SwapperError::from)?; - self.check_tron_approval( - &from_asset, - request.wallet_address.clone(), - amount, - quote_data.gas_limit.clone(), - quote, - provider, - ) - .await + self.check_tron_approval(&from_asset, request.wallet_address.clone(), amount, quote_data.gas_limit.clone(), quote) + .await } _ => Ok((None, None)), } @@ -85,10 +85,9 @@ impl ProxyProvider { token: String, spender: String, amount: U256, - provider: Arc, chain: &Chain, - ) -> Result<(Option, Option), SwapperError> { - let approval = check_approval_erc20(wallet_address, token, spender, amount, provider, chain).await?; + ) -> Result<(Option, Option), SwapperError> { + let approval = check_approval_erc20(wallet_address, token, spender, amount, self.rpc_provider.clone(), chain).await?; let gas_limit = if matches!(approval, ApprovalType::Approve(_)) { Some(DEFAULT_GAS_LIMIT.to_string()) } else { @@ -103,9 +102,8 @@ impl ProxyProvider { wallet_address: String, amount: U256, default_fee_limit: Option, - quote: &SwapperQuote, - provider: Arc, - ) -> Result<(Option, Option), SwapperError> { + quote: &Quote, + ) -> Result<(Option, Option), SwapperError> { let route_data = quote.data.routes.first().map(|r| r.route_data.clone()).ok_or(SwapperError::InvalidRoute)?; let proxy_quote: ProxyQuote = serde_json::from_str(&route_data).map_err(|_| SwapperError::InvalidRoute)?; let spender = proxy_quote.route_data["approveTo"] @@ -116,15 +114,16 @@ impl ProxyProvider { ApprovalType::None } else { let token = from_asset.token_id.clone().unwrap(); - check_approval_tron(&wallet_address, &token, spender, amount, provider.clone()).await? + check_approval_tron(&wallet_address, &token, spender, amount, self.rpc_provider.clone()).await? }; let fee_limit = if matches!(approval, ApprovalType::Approve(_)) { default_fee_limit } else { let tx_data: SymbiosisTransactionData = serde_json::from_value(proxy_quote.route_data["tx"].clone()).map_err(|_| SwapperError::InvalidRoute)?; - let client = TronClient::new(provider.clone()); + let client = create_tron_client(self.rpc_provider.clone()).map_err(|e| SwapperError::NetworkError(e.to_string()))?; let call_value = tx_data.value.unwrap_or_default(); + let call_value_u64 = call_value.parse::().unwrap_or_default(); let energy = client .estimate_energy( &wallet_address, @@ -132,18 +131,106 @@ impl ProxyProvider { &tx_data.function_selector, &tx_data.data, tx_data.fee_limit.unwrap_or_default(), - &call_value, + call_value_u64, ) - .await?; + .await + .map_err(|e| SwapperError::NetworkError(e.to_string()))?; Some(energy.to_string()) }; Ok((approval.approval_data(), fee_limit)) } } +impl ProxyProvider { + fn new_with_path(provider: SwapperProvider, path: &str, assets: Vec, rpc_provider: Arc) -> Self { + let base_url = format!("{PROVIDER_API_URL}/{path}"); + let client = ProxyClient::new(RpcClient::new(base_url, rpc_provider.clone())); + Self::new_with_client(provider, client, assets, rpc_provider) + } + + pub fn new_stonfi_v2(rpc_provider: Arc) -> Self { + Self::new_with_path(SwapperProvider::StonfiV2, "stonfi_v2", vec![SwapperChainAsset::All(Chain::Ton)], rpc_provider) + } + + pub fn new_symbiosis(rpc_provider: Arc) -> Self { + Self::new_with_path(SwapperProvider::Symbiosis, "symbiosis", vec![SwapperChainAsset::All(Chain::Tron)], rpc_provider) + } + + pub fn new_cetus_aggregator(rpc_provider: Arc) -> Self { + Self::new_with_path( + SwapperProvider::CetusAggregator, + "cetus", + vec![SwapperChainAsset::All(Chain::Sui)], + rpc_provider, + ) + } + + pub fn new_mayan(rpc_provider: Arc) -> Self { + let assets = vec![ + SwapperChainAsset::Assets( + Chain::Ethereum, + vec![ + ETHEREUM_USDT.id.clone(), + ETHEREUM_USDC.id.clone(), + ETHEREUM_DAI.id.clone(), + ETHEREUM_USDS.id.clone(), + ETHEREUM_WBTC.id.clone(), + ETHEREUM_WETH.id.clone(), + ETHEREUM_STETH.id.clone(), + ETHEREUM_CBBTC.id.clone(), + ], + ), + SwapperChainAsset::Assets( + Chain::Solana, + vec![ + SOLANA_USDC.id.clone(), + SOLANA_USDT.id.clone(), + SOLANA_USDS.id.clone(), + SOLANA_CBBTC.id.clone(), + SOLANA_WBTC.id.clone(), + SOLANA_JITO_SOL.id.clone(), + ], + ), + SwapperChainAsset::Assets(Chain::Sui, vec![SUI_USDC.id.clone(), SUI_SBUSDT.id.clone(), SUI_WAL.id.clone()]), + SwapperChainAsset::Assets( + Chain::SmartChain, + vec![SMARTCHAIN_USDT.id.clone(), SMARTCHAIN_USDC.id.clone(), SMARTCHAIN_WBTC.id.clone()], + ), + SwapperChainAsset::Assets( + Chain::Base, + vec![BASE_USDC.id.clone(), BASE_CBBTC.id.clone(), BASE_WBTC.id.clone(), BASE_USDS.id.clone()], + ), + SwapperChainAsset::Assets(Chain::Polygon, vec![POLYGON_USDC.id.clone(), POLYGON_USDT.id.clone()]), + SwapperChainAsset::Assets(Chain::AvalancheC, vec![AVALANCHE_USDT.id.clone(), AVALANCHE_USDC.id.clone()]), + SwapperChainAsset::Assets(Chain::Arbitrum, vec![ARBITRUM_USDC.id.clone(), ARBITRUM_USDT.id.clone()]), + SwapperChainAsset::Assets(Chain::Optimism, vec![OPTIMISM_USDC.id.clone(), OPTIMISM_USDT.id.clone()]), + SwapperChainAsset::Assets(Chain::Linea, vec![LINEA_USDC.id.clone(), LINEA_USDT.id.clone()]), + SwapperChainAsset::Assets(Chain::Unichain, vec![UNICHAIN_USDC.id.clone(), UNICHAIN_DAI.id.clone()]), + ]; + + 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::Manta), + SwapperChainAsset::All(Chain::Berachain), + ], + rpc_provider, + ) + } +} + #[async_trait] -impl Swapper for ProxyProvider { - fn provider(&self) -> &SwapperProviderType { +impl Swapper for ProxyProvider +where + C: Client + Clone + Send + Sync + Debug + 'static, +{ + fn provider(&self) -> &ProviderType { &self.provider } @@ -151,8 +238,7 @@ impl Swapper for ProxyProvider { self.assets.clone() } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result { - let client = ProxyClient::new(provider); + async fn fetch_quote(&self, request: &QuoteRequest) -> Result { let quote_request = ProxyQuoteRequest { from_address: request.wallet_address.clone(), to_address: request.destination_address.clone(), @@ -163,14 +249,14 @@ impl Swapper for ProxyProvider { slippage_bps: request.options.slippage.bps, }; - let quote = client.get_quote(&self.url, quote_request.clone()).await?; + let quote = self.client.get_quote(quote_request.clone()).await?; - Ok(SwapperQuote { + Ok(Quote { from_value: request.value.clone(), to_value: quote.output_value.clone(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider().clone(), - routes: vec![SwapperRoute { + routes: vec![Route { input: request.from_asset.asset_id(), output: request.to_asset.asset_id(), route_data: serde_json::to_string("e).unwrap(), @@ -183,13 +269,12 @@ impl Swapper for ProxyProvider { }) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, provider: Arc, _data: FetchQuoteData) -> Result { + async fn fetch_quote_data(&self, quote: &Quote, _data: FetchQuoteData) -> Result { let routes = quote.data.clone().routes; let route_data: ProxyQuote = serde_json::from_str(&routes.first().unwrap().route_data).map_err(|_| SwapperError::InvalidRoute)?; - let client = ProxyClient::new(provider.clone()); - let data = client.get_quote_data(&self.url, route_data).await?; - let (approval, gas_limit) = self.check_approval(quote, &data, provider).await?; + let data = self.client.get_quote_data(route_data).await?; + let (approval, gas_limit) = self.check_approval(quote, &data).await?; Ok(SwapperQuoteData { to: data.to, @@ -200,10 +285,10 @@ impl Swapper for ProxyProvider { }) } - async fn get_swap_result(&self, chain: Chain, transaction_hash: &str, provider: Arc) -> Result { + async fn get_swap_result(&self, chain: Chain, transaction_hash: &str) -> Result { match self.provider.id { SwapperProvider::Mayan => { - let client = MayanExplorer::new(provider); + let client = MayanExplorer::new(self.rpc_provider.clone()); let result = client.get_transaction_status(transaction_hash).await?; let swap_status = result.client_status.swap_status(); @@ -214,7 +299,7 @@ impl Swapper for ProxyProvider { MayanClientStatus::Refunded | MayanClientStatus::InProgress => (dest_chain, None), }; - Ok(SwapperSwapResult { + Ok(SwapResult { status: swap_status, from_chain: chain, from_tx_hash: transaction_hash.to_string(), @@ -238,24 +323,23 @@ impl Swapper for ProxyProvider { mod swap_integration_tests { use super::*; use crate::{ - network::alien_provider::NativeProvider, - swapper::{SwapperMode, asset::SUI_USDC_TOKEN_ID, models::SwapperOptions, remote_models::SwapperQuoteAsset}, + alien::reqwest_provider::NativeProvider, + {SwapperMode, SwapperQuoteAsset, asset::SUI_USDC_TOKEN_ID, models::Options}, }; use primitives::AssetId; #[tokio::test] async fn test_mayan_provider_fetch_quote() -> Result<(), SwapperError> { - let provider = ProxyProvider::new_mayan(); + let rpc_provider = Arc::new(NativeProvider::default()); + let provider = ProxyProvider::new_mayan(rpc_provider); - let network_provider = Arc::new(NativeProvider::default()); - - let options = SwapperOptions { + let options = Options { slippage: 200.into(), fee: None, preferred_providers: vec![], }; - let request = SwapperQuoteRequest { + let request = QuoteRequest { from_asset: SwapperQuoteAsset::from(AssetId::from_chain(Chain::Ethereum)), to_asset: SwapperQuoteAsset::from(AssetId::from_chain(Chain::Solana)), wallet_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".to_string(), @@ -265,7 +349,7 @@ mod swap_integration_tests { options, }; - let quote = provider.fetch_quote(&request, network_provider).await?; + let quote = provider.fetch_quote(&request).await?; assert_eq!(quote.from_value, request.value); assert!(quote.to_value.parse::().unwrap() > 0); @@ -284,17 +368,16 @@ mod swap_integration_tests { #[tokio::test] async fn test_cetus_provider_fetch_quote() -> Result<(), SwapperError> { - let provider = ProxyProvider::new_cetus_aggregator(); - - let network_provider = Arc::new(NativeProvider::default()); + let rpc_provider = Arc::new(NativeProvider::default()); + let provider = ProxyProvider::new_cetus_aggregator(rpc_provider); - let options = SwapperOptions { + let options = Options { slippage: 50.into(), fee: None, preferred_providers: vec![], }; - let request = SwapperQuoteRequest { + let request = QuoteRequest { from_asset: SwapperQuoteAsset::from(AssetId::from_chain(Chain::Sui)), to_asset: SwapperQuoteAsset::from(AssetId::from(Chain::Sui, Some(SUI_USDC_TOKEN_ID.to_string()))), wallet_address: "0xa9bd0493f9bd1f792a4aedc1f99d54535a75a46c38fd56a8f2c6b7c8d75817a1".to_string(), @@ -304,7 +387,7 @@ mod swap_integration_tests { options, }; - let quote = provider.fetch_quote(&request, network_provider).await?; + let quote = provider.fetch_quote(&request).await?; assert_eq!(quote.from_value, request.value); assert!(quote.to_value.parse::().unwrap() > 0); diff --git a/crates/swapper/src/proxy/provider_factory.rs b/crates/swapper/src/proxy/provider_factory.rs new file mode 100644 index 000000000..f99b8f278 --- /dev/null +++ b/crates/swapper/src/proxy/provider_factory.rs @@ -0,0 +1,24 @@ +use crate::alien::{RpcClient, RpcProvider}; +use std::sync::Arc; + +use super::provider::ProxyProvider; + +pub fn new_stonfi_v2(rpc_provider: Arc) -> ProxyProvider { + ProxyProvider::new_stonfi_v2(rpc_provider) +} + +pub fn new_symbiosis(rpc_provider: Arc) -> ProxyProvider { + ProxyProvider::new_symbiosis(rpc_provider) +} + +pub fn new_cetus_aggregator(rpc_provider: Arc) -> ProxyProvider { + ProxyProvider::new_cetus_aggregator(rpc_provider) +} + +pub fn new_mayan(rpc_provider: Arc) -> ProxyProvider { + ProxyProvider::new_mayan(rpc_provider) +} + +pub fn new_relay(rpc_provider: Arc) -> ProxyProvider { + ProxyProvider::new_relay(rpc_provider) +} diff --git a/gemstone/src/swapper/proxy/symbiosis/mod.rs b/crates/swapper/src/proxy/symbiosis/mod.rs similarity index 100% rename from gemstone/src/swapper/proxy/symbiosis/mod.rs rename to crates/swapper/src/proxy/symbiosis/mod.rs diff --git a/gemstone/src/swapper/proxy/symbiosis/model.rs b/crates/swapper/src/proxy/symbiosis/model.rs similarity index 100% rename from gemstone/src/swapper/proxy/symbiosis/model.rs rename to crates/swapper/src/proxy/symbiosis/model.rs diff --git a/gemstone/src/swapper/proxy/test/eth_to_sui_swift.json b/crates/swapper/src/proxy/test/eth_to_sui_swift.json similarity index 100% rename from gemstone/src/swapper/proxy/test/eth_to_sui_swift.json rename to crates/swapper/src/proxy/test/eth_to_sui_swift.json diff --git a/gemstone/src/swapper/proxy/test/mctp_pending.json b/crates/swapper/src/proxy/test/mctp_pending.json similarity index 100% rename from gemstone/src/swapper/proxy/test/mctp_pending.json rename to crates/swapper/src/proxy/test/mctp_pending.json diff --git a/gemstone/src/swapper/proxy/test/swift_refunded.json b/crates/swapper/src/proxy/test/swift_refunded.json similarity index 100% rename from gemstone/src/swapper/proxy/test/swift_refunded.json rename to crates/swapper/src/proxy/test/swift_refunded.json diff --git a/gemstone/src/swapper/slippage.rs b/crates/swapper/src/slippage.rs similarity index 100% rename from gemstone/src/swapper/slippage.rs rename to crates/swapper/src/slippage.rs diff --git a/gemstone/src/swapper/mod.rs b/crates/swapper/src/swapper.rs similarity index 74% rename from gemstone/src/swapper/mod.rs rename to crates/swapper/src/swapper.rs index 34bf1951f..278f39ed6 100644 --- a/gemstone/src/swapper/mod.rs +++ b/crates/swapper/src/swapper.rs @@ -1,44 +1,15 @@ -use crate::{config::swap_config::DEFAULT_STABLE_SWAP_REFERRAL_BPS, debug_println, network::AlienProvider, swapper::proxy::provider::ProxyProvider}; - +use crate::{ + AssetList, FetchQuoteData, Permit2ApprovalData, ProviderType, Quote, QuoteRequest, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, + SwapperProviderMode, SwapperQuoteData, across, alien::RpcProvider, chainflip, config::DEFAULT_STABLE_SWAP_REFERRAL_BPS, hyperliquid, jupiter, + pancakeswap_aptos, proxy::provider_factory, thorchain, uniswap, +}; use num_traits::ToPrimitive; -use std::{borrow::Cow, fmt::Debug, sync::Arc}; - -mod approval; -mod chainlink; -mod custom_types; -mod eth_address; -mod permit2_data; -mod swapper_trait; - -#[cfg(test)] -pub mod testkit; - -pub mod across; -pub mod asset; -pub mod cetus; -pub mod chainflip; -pub mod error; -pub mod hyperliquid; -pub mod jupiter; -pub mod models; -pub mod pancakeswap_aptos; -pub mod proxy; -pub mod remote_models; -pub mod slippage; -pub mod thorchain; -pub mod uniswap; - -pub use error::*; -pub use models::*; -pub use remote_models::*; -pub use swapper_trait::*; - use primitives::{AssetId, Chain, EVMChain}; -use std::collections::HashSet; +use std::{borrow::Cow, collections::HashSet, fmt::Debug, sync::Arc}; -#[derive(Debug, uniffi::Object)] +#[derive(Debug)] pub struct GemSwapper { - pub rpc_provider: Arc, + pub rpc_provider: Arc, pub swappers: Vec>, } @@ -58,7 +29,7 @@ impl GemSwapper { fn filter_supported_assets(supported_assets: Vec, asset_id: AssetId) -> bool { supported_assets.into_iter().any(|x| match x { - SwapperChainAsset::All(_) => false, + SwapperChainAsset::All(chain) => chain == asset_id.chain, SwapperChainAsset::Assets(chain, assets) => chain == asset_id.chain || assets.contains(&asset_id), }) } @@ -85,7 +56,7 @@ impl GemSwapper { gas_limit } - fn transform_request<'a>(request: &'a SwapperQuoteRequest) -> Cow<'a, SwapperQuoteRequest> { + fn transform_request<'a>(request: &'a QuoteRequest) -> Cow<'a, QuoteRequest> { if !Self::is_stable_swap(request) || request.options.fee.is_none() { return Cow::Borrowed(request); } @@ -98,7 +69,7 @@ impl GemSwapper { Cow::Owned(updated_request) } - fn is_stable_swap(request: &SwapperQuoteRequest) -> bool { + fn is_stable_swap(request: &QuoteRequest) -> bool { let from_symbol = request.from_asset.symbol.to_ascii_uppercase(); let to_symbol = request.to_asset.symbol.to_ascii_uppercase(); @@ -106,31 +77,28 @@ impl GemSwapper { } } -#[uniffi::export] impl GemSwapper { - #[uniffi::constructor] - pub fn new(rpc_provider: Arc) -> Self { - Self { - rpc_provider, - swappers: vec![ - Box::new(uniswap::universal_router::new_uniswap_v3()), - Box::new(uniswap::universal_router::new_uniswap_v4()), - Box::new(uniswap::universal_router::new_pancakeswap()), - Box::new(thorchain::ThorChain::default()), - Box::new(jupiter::Jupiter::default()), - Box::new(across::Across::default()), - Box::new(hyperliquid::HyperCoreBridge::default()), - Box::new(uniswap::universal_router::new_oku()), - Box::new(uniswap::universal_router::new_wagmi()), - Box::new(pancakeswap_aptos::PancakeSwapAptos::default()), - Box::new(ProxyProvider::new_stonfi_v2()), - Box::new(ProxyProvider::new_mayan()), - Box::new(chainflip::ChainflipProvider::default()), - Box::new(ProxyProvider::new_cetus_aggregator()), - Box::new(ProxyProvider::new_relay()), - Box::new(uniswap::universal_router::new_aerodrome()), - ], - } + pub fn new(rpc_provider: Arc) -> Self { + let swappers: Vec> = vec![ + uniswap::default::boxed_uniswap_v3(rpc_provider.clone()), + uniswap::default::boxed_uniswap_v4(rpc_provider.clone()), + uniswap::default::boxed_pancakeswap(rpc_provider.clone()), + Box::new(thorchain::ThorChain::new(rpc_provider.clone())), + Box::new(jupiter::Jupiter::new(rpc_provider.clone())), + Box::new(across::Across::new(rpc_provider.clone())), + Box::new(hyperliquid::HyperCoreBridge::new()), + uniswap::default::boxed_oku(rpc_provider.clone()), + uniswap::default::boxed_wagmi(rpc_provider.clone()), + Box::new(pancakeswap_aptos::PancakeSwapAptos::new(rpc_provider.clone())), + Box::new(provider_factory::new_stonfi_v2(rpc_provider.clone())), + Box::new(provider_factory::new_mayan(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())), + uniswap::default::boxed_aerodrome(rpc_provider.clone()), + ]; + + Self { rpc_provider, swappers } } pub fn supported_chains(&self) -> Vec { @@ -142,7 +110,7 @@ impl GemSwapper { .collect() } - pub fn supported_chains_for_from_asset(&self, asset_id: &AssetId) -> SwapperAssetList { + pub fn supported_chains_for_from_asset(&self, asset_id: &AssetId) -> AssetList { let chains: Vec = vec![asset_id.chain]; let mut asset_ids: Vec = Vec::new(); @@ -158,14 +126,14 @@ impl GemSwapper { } }); } - SwapperAssetList { chains, asset_ids } + AssetList { chains, asset_ids } } - pub fn get_providers(&self) -> Vec { + pub fn get_providers(&self) -> Vec { self.swappers.iter().map(|x| x.provider().clone()).collect() } - pub async fn fetch_quote(&self, request: &SwapperQuoteRequest) -> Result, SwapperError> { + pub async fn fetch_quote(&self, request: &QuoteRequest) -> Result, SwapperError> { if request.from_asset.id == request.to_asset.id { return Err(SwapperError::NotSupportedPair); } @@ -185,15 +153,13 @@ impl GemSwapper { } let request_for_quote = Self::transform_request(request); - let quotes_futures = providers - .into_iter() - .map(|x| x.fetch_quote(request_for_quote.as_ref(), self.rpc_provider.clone())); + let quotes_futures = providers.into_iter().map(|x| x.fetch_quote(request_for_quote.as_ref())); let quotes = futures::future::join_all(quotes_futures.into_iter().map(|fut| async { match &fut.await { Ok(quote) => Some(quote.clone()), Err(_err) => { - debug_println!("fetch_quote error: {:?}", _err); + tracing::debug!("fetch_quote error: {:?}", _err); None } } @@ -210,42 +176,47 @@ impl GemSwapper { Ok(quotes) } - pub async fn fetch_quote_by_provider(&self, provider: SwapperProvider, request: SwapperQuoteRequest) -> Result { + pub async fn fetch_quote_by_provider(&self, provider: SwapperProvider, request: QuoteRequest) -> Result { let provider = self.get_swapper_by_provider(&provider).ok_or(SwapperError::NoAvailableProvider)?; let request_for_quote = Self::transform_request(&request); - provider.fetch_quote(request_for_quote.as_ref(), self.rpc_provider.clone()).await + provider.fetch_quote(request_for_quote.as_ref()).await } - pub async fn fetch_permit2_for_quote(&self, quote: &SwapperQuote) -> Result, SwapperError> { + pub async fn fetch_permit2_for_quote(&self, quote: &Quote) -> Result, SwapperError> { let provider = self.get_swapper_by_provider("e.data.provider.id).ok_or(SwapperError::NoAvailableProvider)?; - provider.fetch_permit2_for_quote(quote, self.rpc_provider.clone()).await + provider.fetch_permit2_for_quote(quote).await } - pub async fn fetch_quote_data(&self, quote: &SwapperQuote, data: FetchQuoteData) -> Result { + pub async fn fetch_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { let provider = self.get_swapper_by_provider("e.data.provider.id).ok_or(SwapperError::NoAvailableProvider)?; - let mut quote_data = provider.fetch_quote_data(quote, self.rpc_provider.clone(), data).await?; + let mut quote_data = provider.fetch_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)); } Ok(quote_data) } - pub async fn get_swap_result(&self, chain: Chain, swap_provider: SwapperProvider, transaction_hash: &str) -> Result { + pub async fn get_swap_result(&self, chain: Chain, swap_provider: SwapperProvider, transaction_hash: &str) -> Result { let provider = self.get_swapper_by_provider(&swap_provider).ok_or(SwapperError::NoAvailableProvider)?; - provider.get_swap_result(chain, transaction_hash, self.rpc_provider.clone()).await + provider.get_swap_result(chain, transaction_hash).await } } -#[cfg(test)] +#[cfg(all(test, feature = "reqwest_provider"))] mod tests { use super::*; - use crate::config::swap_config::{DEFAULT_STABLE_SWAP_REFERRAL_BPS, DEFAULT_SWAP_FEE_BPS, SwapReferralFee, SwapReferralFees}; + use crate::{ + Options, SwapperMode, SwapperQuoteAsset, SwapperSlippage, SwapperSlippageMode, + alien::reqwest_provider::NativeProvider, + config::{DEFAULT_STABLE_SWAP_REFERRAL_BPS, DEFAULT_SWAP_FEE_BPS, ReferralFee, ReferralFees}, + uniswap::default::{new_pancakeswap, new_uniswap_v3}, + }; use primitives::asset_constants::USDT_ETH_ASSET_ID; - use std::{borrow::Cow, collections::BTreeSet, vec}; + use std::{borrow::Cow, collections::BTreeSet, sync::Arc, vec}; - fn build_request(from_symbol: &str, to_symbol: &str, fee: Option) -> SwapperQuoteRequest { - SwapperQuoteRequest { + fn build_request(from_symbol: &str, to_symbol: &str, fee: Option) -> QuoteRequest { + QuoteRequest { from_asset: SwapperQuoteAsset { id: format!("{}_asset", from_symbol), symbol: from_symbol.to_string(), @@ -260,7 +231,7 @@ mod tests { destination_address: "0xwallet".into(), value: "1000000".into(), mode: SwapperMode::ExactIn, - options: SwapperOptions { + options: Options { slippage: SwapperSlippage { bps: 100, mode: SwapperSlippageMode::Exact, @@ -283,7 +254,7 @@ mod tests { // Cross chain swaps (same chain will be filtered out) let filtered = providers .iter() - .filter(|x| GemSwapper::filter_by_provider_mode(SwapperProviderType::new(**x).mode(), Chain::Ethereum, Chain::Optimism)) + .filter(|x| GemSwapper::filter_by_provider_mode(ProviderType::new(**x).mode(), Chain::Ethereum, Chain::Optimism)) .cloned() .collect::>(); @@ -292,11 +263,12 @@ mod tests { #[test] fn test_filter_by_supported_chains() { + let provider = Arc::new(NativeProvider::default()); let swappers: Vec> = vec![ - Box::new(uniswap::universal_router::new_uniswap_v3()), - Box::new(uniswap::universal_router::new_pancakeswap()), - Box::new(thorchain::ThorChain::default()), - Box::new(jupiter::Jupiter::default()), + Box::new(new_uniswap_v3(provider.clone())), + Box::new(new_pancakeswap(provider.clone())), + Box::new(thorchain::ThorChain::new(provider.clone())), + Box::new(jupiter::Jupiter::new(provider)), ]; let from_chain = Chain::Ethereum; @@ -354,6 +326,9 @@ mod tests { fn test_filter_supported_assets() { let asset_id = AssetId::from_chain(Chain::Ethereum); let asset_id_usdt: AssetId = USDT_ETH_ASSET_ID.into(); + let supported_assets_all = vec![SwapperChainAsset::All(Chain::Ethereum)]; + assert!(GemSwapper::filter_supported_assets(supported_assets_all, asset_id.clone())); + let supported_assets = vec![ SwapperChainAsset::All(Chain::Ethereum), SwapperChainAsset::Assets( @@ -377,32 +352,32 @@ mod tests { #[test] fn test_adjust_request_for_stable_swap_updates_referral_fees() { - let referral_fees = SwapReferralFees { - evm: SwapReferralFee { + let referral_fees = ReferralFees { + evm: ReferralFee { address: "0xevm".into(), bps: DEFAULT_SWAP_FEE_BPS, }, - evm_bridge: SwapReferralFee { + evm_bridge: ReferralFee { address: "0xbridge".into(), bps: DEFAULT_SWAP_FEE_BPS, }, - solana: SwapReferralFee { + solana: ReferralFee { address: "SolanaReferral".into(), bps: DEFAULT_SWAP_FEE_BPS, }, - thorchain: SwapReferralFee { + thorchain: ReferralFee { address: "ThorReferral".into(), bps: DEFAULT_SWAP_FEE_BPS, }, - sui: SwapReferralFee { + sui: ReferralFee { address: "SuiReferral".into(), bps: DEFAULT_SWAP_FEE_BPS, }, - ton: SwapReferralFee { + ton: ReferralFee { address: "TonReferral".into(), bps: DEFAULT_SWAP_FEE_BPS, }, - tron: SwapReferralFee { + tron: ReferralFee { address: "TronReferral".into(), bps: DEFAULT_SWAP_FEE_BPS, }, diff --git a/gemstone/src/swapper/swapper_trait.rs b/crates/swapper/src/swapper_trait.rs similarity index 57% rename from gemstone/src/swapper/swapper_trait.rs rename to crates/swapper/src/swapper_trait.rs index f70498efe..6cdd2808d 100644 --- a/gemstone/src/swapper/swapper_trait.rs +++ b/crates/swapper/src/swapper_trait.rs @@ -1,24 +1,23 @@ use super::{ + SwapperProviderMode, SwapperQuoteData, error::SwapperError, - models::{FetchQuoteData, Permit2ApprovalData, SwapperChainAsset, SwapperProviderType, SwapperQuote, SwapperQuoteRequest, SwapperSwapResult}, - remote_models::{SwapperProviderMode, SwapperQuoteData}, + models::{FetchQuoteData, Permit2ApprovalData, ProviderType, Quote, QuoteRequest, SwapResult, SwapperChainAsset}, }; -use crate::network::AlienProvider; use async_trait::async_trait; -use std::{fmt::Debug, sync::Arc}; +use std::fmt::Debug; use primitives::{Chain, swap::SwapStatus}; #[async_trait] pub trait Swapper: Send + Sync + Debug { - fn provider(&self) -> &SwapperProviderType; + fn provider(&self) -> &ProviderType; fn supported_assets(&self) -> Vec; - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result; - async fn fetch_permit2_for_quote(&self, _quote: &SwapperQuote, _provider: Arc) -> Result, SwapperError> { + async fn fetch_quote(&self, request: &QuoteRequest) -> Result; + async fn fetch_permit2_for_quote(&self, _quote: &Quote) -> Result, SwapperError> { Ok(None) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, provider: Arc, data: FetchQuoteData) -> Result; - async fn get_swap_result(&self, chain: Chain, transaction_hash: &str, _provider: Arc) -> Result { + async fn fetch_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result; + async fn get_swap_result(&self, chain: Chain, transaction_hash: &str) -> Result { if self.provider().mode() == SwapperProviderMode::OnChain { Ok(self.get_onchain_swap_status(chain, transaction_hash)) } else { @@ -27,8 +26,8 @@ pub trait Swapper: Send + Sync + Debug { } /// Default OnChain provider swap status implementation - fn get_onchain_swap_status(&self, chain: Chain, transaction_hash: &str) -> SwapperSwapResult { - SwapperSwapResult { + fn get_onchain_swap_status(&self, chain: Chain, transaction_hash: &str) -> SwapResult { + SwapResult { status: SwapStatus::Completed, from_chain: chain, from_tx_hash: transaction_hash.to_string(), diff --git a/gemstone/src/swapper/testkit.rs b/crates/swapper/src/testkit.rs similarity index 64% rename from gemstone/src/swapper/testkit.rs rename to crates/swapper/src/testkit.rs index 927f83a3b..26485cb66 100644 --- a/gemstone/src/swapper/testkit.rs +++ b/crates/swapper/src/testkit.rs @@ -1,50 +1,50 @@ use crate::{ - config::swap_config::{SwapReferralFee, SwapReferralFees}, - swapper::{SwapperSlippage, SwapperSlippageMode}, + SwapperSlippage, SwapperSlippageMode, + config::{ReferralFee, ReferralFees}, }; -use super::{SwapperMode, SwapperOptions, SwapperQuoteRequest}; +use super::{Options, QuoteRequest, SwapperMode}; use primitives::AssetId; -pub fn mock_quote(from_asset: AssetId, to_asset: AssetId) -> SwapperQuoteRequest { - SwapperQuoteRequest { +pub fn mock_quote(from_asset: AssetId, to_asset: AssetId) -> QuoteRequest { + QuoteRequest { from_asset: from_asset.into(), to_asset: to_asset.into(), wallet_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), value: "1000000".into(), mode: SwapperMode::ExactIn, - options: SwapperOptions { + options: Options { slippage: SwapperSlippage { mode: SwapperSlippageMode::Auto, bps: 100, }, - fee: Some(SwapReferralFees { - evm: SwapReferralFee { + fee: Some(ReferralFees { + evm: ReferralFee { address: "g1".into(), bps: 100, }, - evm_bridge: SwapReferralFee { + evm_bridge: ReferralFee { address: "g1".into(), bps: 100, }, - solana: SwapReferralFee { + solana: ReferralFee { address: "g1".into(), bps: 100, }, - thorchain: SwapReferralFee { + thorchain: ReferralFee { address: "g1".into(), bps: 100, }, - sui: SwapReferralFee { + sui: ReferralFee { address: "g1".into(), bps: 100, }, - ton: SwapReferralFee { + ton: ReferralFee { address: "g1".into(), bps: 100, }, - tron: SwapReferralFee { + tron: ReferralFee { address: "g1".into(), bps: 100, }, diff --git a/gemstone/src/swapper/thorchain/asset.rs b/crates/swapper/src/thorchain/asset.rs similarity index 99% rename from gemstone/src/swapper/thorchain/asset.rs rename to crates/swapper/src/thorchain/asset.rs index 4fabbb810..f3da53a72 100644 --- a/gemstone/src/swapper/thorchain/asset.rs +++ b/crates/swapper/src/thorchain/asset.rs @@ -1,6 +1,6 @@ use primitives::{Asset, AssetId}; -use crate::swapper::asset::*; +use crate::asset::*; use super::chain::THORChainName; diff --git a/gemstone/src/swapper/thorchain/chain.rs b/crates/swapper/src/thorchain/chain.rs similarity index 100% rename from gemstone/src/swapper/thorchain/chain.rs rename to crates/swapper/src/thorchain/chain.rs diff --git a/crates/swapper/src/thorchain/client.rs b/crates/swapper/src/thorchain/client.rs new file mode 100644 index 000000000..5242e9f47 --- /dev/null +++ b/crates/swapper/src/thorchain/client.rs @@ -0,0 +1,66 @@ +use super::{ + asset::THORChainAsset, + model::{InboundAddress, QuoteSwapRequest, QuoteSwapResponse, Transaction}, +}; +use crate::{SwapperError, alien::X_CACHE_TTL}; +use gem_client::{Client, ClientError}; +use serde_urlencoded; +use std::{collections::HashMap, fmt::Debug}; + +#[derive(Clone, Debug)] +pub struct ThorChainSwapClient +where + C: Client + Clone + Send + Sync + Debug + 'static, +{ + client: C, +} + +impl ThorChainSwapClient +where + C: Client + Clone + Send + Sync + Debug + 'static, +{ + pub fn new(client: C) -> Self { + Self { client } + } + + pub async fn get_quote( + &self, + from_asset: THORChainAsset, + to_asset: THORChainAsset, + value: String, + streaming_interval: i64, + streaming_quantity: i64, + affiliate: String, + affiliate_bps: i64, + ) -> Result { + let params = QuoteSwapRequest { + from_asset: from_asset.asset_name(), + to_asset: to_asset.asset_name(), + amount: value, + affiliate, + affiliate_bps, + streaming_interval, + streaming_quantity, + }; + let query = serde_urlencoded::to_string(params).map_err(SwapperError::from)?; + let path = format!("/thorchain/quote/swap?{query}"); + self.client.get(&path).await.map_err(map_client_error) + } + + pub async fn get_inbound_addresses(&self) -> Result, SwapperError> { + let headers = HashMap::from([(X_CACHE_TTL.to_string(), "600".to_string())]); + self.client + .get_with_headers("/thorchain/inbound_addresses", Some(headers)) + .await + .map_err(map_client_error) + } + + pub async fn get_transaction_status(&self, transaction_hash: &str) -> Result { + let path = format!("/thorchain/tx/{transaction_hash}"); + self.client.get(&path).await.map_err(map_client_error) + } +} + +fn map_client_error(err: ClientError) -> SwapperError { + SwapperError::from(err) +} diff --git a/crates/swapper/src/thorchain/default.rs b/crates/swapper/src/thorchain/default.rs new file mode 100644 index 000000000..4837c81db --- /dev/null +++ b/crates/swapper/src/thorchain/default.rs @@ -0,0 +1,12 @@ +use super::{ThorChain, client::ThorChainSwapClient}; +use crate::alien::{RpcClient, RpcProvider}; +use primitives::Chain; +use std::sync::Arc; + +impl ThorChain { + pub fn new(rpc_provider: Arc) -> Self { + let endpoint = rpc_provider.get_endpoint(Chain::Thorchain).expect("Failed to get Thorchain endpoint"); + let swap_client = ThorChainSwapClient::new(RpcClient::new(endpoint, rpc_provider.clone())); + Self::with_client(swap_client, rpc_provider) + } +} diff --git a/gemstone/src/swapper/thorchain/memo.rs b/crates/swapper/src/thorchain/memo.rs similarity index 100% rename from gemstone/src/swapper/thorchain/memo.rs rename to crates/swapper/src/thorchain/memo.rs diff --git a/gemstone/src/swapper/thorchain/mod.rs b/crates/swapper/src/thorchain/mod.rs similarity index 74% rename from gemstone/src/swapper/thorchain/mod.rs rename to crates/swapper/src/thorchain/mod.rs index 357811cce..e5cae6d29 100644 --- a/gemstone/src/swapper/thorchain/mod.rs +++ b/crates/swapper/src/thorchain/mod.rs @@ -1,6 +1,7 @@ mod asset; mod chain; mod client; +mod default; mod memo; mod model; mod provider; @@ -8,9 +9,12 @@ mod provider; use chain::THORChainName; use num_bigint::BigInt; use primitives::Chain; -use std::str::FromStr; +use std::{str::FromStr, sync::Arc}; -use super::{SwapperProvider, SwapperProviderType}; +use crate::alien::RpcProvider; +use gem_client::Client; + +use super::{ProviderType, SwapperProvider}; const QUOTE_MINIMUM: i64 = 0; const QUOTE_INTERVAL: i64 = 1; @@ -21,19 +25,27 @@ const OUTBOUND_DELAY_SECONDS: u32 = 60; const DEFAULT_DEPOSIT_GAS_LIMIT: u64 = 90_000; #[derive(Debug)] -pub struct ThorChain { - pub provider: SwapperProviderType, +pub struct ThorChain +where + C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, +{ + pub provider: ProviderType, + pub rpc_provider: Arc, + pub(crate) swap_client: client::ThorChainSwapClient, } -impl Default for ThorChain { - fn default() -> Self { +impl ThorChain +where + C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, +{ + pub fn with_client(swap_client: client::ThorChainSwapClient, rpc_provider: Arc) -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::Thorchain), + provider: ProviderType::new(SwapperProvider::Thorchain), + rpc_provider, + swap_client, } } -} -impl ThorChain { fn data(&self, chain: THORChainName, memo: String) -> String { if chain.is_evm_chain() { return hex::encode(memo.as_bytes()); @@ -64,13 +76,15 @@ impl ThorChain { } } -#[cfg(test)] +#[cfg(all(test, feature = "reqwest_provider"))] mod tests { use super::*; + use crate::alien::reqwest_provider::NativeProvider; + use std::sync::Arc; #[test] fn test_data() { - let thorchain = ThorChain::default(); + let thorchain = ThorChain::new(Arc::new(NativeProvider::default())); let memo = "test".to_string(); let result = thorchain.data(THORChainName::Ethereum, memo.clone()); @@ -82,7 +96,7 @@ mod tests { #[test] fn test_value_from() { - let thorchain = ThorChain::default(); + let thorchain = ThorChain::new(Arc::new(NativeProvider::default())); let value = "1000000000".to_string(); @@ -101,7 +115,7 @@ mod tests { #[test] fn test_value_to() { - let thorchain = ThorChain::default(); + let thorchain = ThorChain::new(Arc::new(NativeProvider::default())); let value = "10000000".to_string(); @@ -120,7 +134,7 @@ mod tests { #[test] fn test_get_eta_in_seconds() { - let thorchain = ThorChain::default(); + let thorchain = ThorChain::new(Arc::new(NativeProvider::default())); let eta = thorchain.get_eta_in_seconds(Chain::Bitcoin, None); assert_eq!(eta, 660); diff --git a/gemstone/src/swapper/thorchain/model.rs b/crates/swapper/src/thorchain/model.rs similarity index 100% rename from gemstone/src/swapper/thorchain/model.rs rename to crates/swapper/src/thorchain/model.rs diff --git a/gemstone/src/swapper/thorchain/provider.rs b/crates/swapper/src/thorchain/provider.rs similarity index 82% rename from gemstone/src/swapper/thorchain/provider.rs rename to crates/swapper/src/thorchain/provider.rs index 88088f40f..188ebf51c 100644 --- a/gemstone/src/swapper/thorchain/provider.rs +++ b/crates/swapper/src/thorchain/provider.rs @@ -1,33 +1,32 @@ use std::{ str::FromStr, - sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; use alloy_primitives::{Address, U256, hex::encode_prefixed as HexEncode}; use alloy_sol_types::SolCall; use async_trait::async_trait; +use gem_client::Client; use gem_evm::thorchain::contracts::RouterInterface; -use primitives::Chain; +use primitives::{Chain, swap::ApprovalData}; use super::{ - DEFAULT_DEPOSIT_GAS_LIMIT, QUOTE_INTERVAL, QUOTE_MINIMUM, QUOTE_QUANTITY, ThorChain, asset::THORChainAsset, chain::THORChainName, - client::ThorChainSwapClient, memo::ThorchainMemo, model::RouteData, + DEFAULT_DEPOSIT_GAS_LIMIT, QUOTE_INTERVAL, QUOTE_MINIMUM, QUOTE_QUANTITY, ThorChain, asset::THORChainAsset, chain::THORChainName, memo::ThorchainMemo, + model::RouteData, }; use crate::{ - models::GemApprovalData, - network::AlienProvider, - swapper::{ - FetchQuoteData, Swapper, SwapperChainAsset, SwapperError, SwapperProviderData, SwapperProviderType, SwapperQuote, SwapperQuoteData, - SwapperQuoteRequest, SwapperRoute, SwapperSwapResult, approval::check_approval_erc20, asset::*, - }, + FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperQuoteData, + approval::check_approval_erc20, asset::*, }; const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; #[async_trait] -impl Swapper for ThorChain { - fn provider(&self) -> &SwapperProviderType { +impl Swapper for ThorChain +where + C: Client + Clone + Send + Sync + std::fmt::Debug + 'static, +{ + fn provider(&self) -> &ProviderType { &self.provider } @@ -57,10 +56,7 @@ impl Swapper for ThorChain { .collect() } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result { - let endpoint = provider.get_endpoint(Chain::Thorchain).map_err(SwapperError::from)?; - let client = ThorChainSwapClient::new(provider.clone()); - + async fn fetch_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)?; @@ -69,7 +65,7 @@ impl Swapper for ThorChain { // thorchain is not included in inbound addresses if from_asset.chain != THORChainName::Thorchain { // min fee validation - let inbound_addresses = client.get_inbound_addresses(endpoint.as_str()).await?; + let inbound_addresses = self.swap_client.get_inbound_addresses().await?; let from_inbound_address = &inbound_addresses .iter() .find(|address| address.chain == from_asset.chain.long_name()) @@ -86,9 +82,9 @@ impl Swapper for ThorChain { let fee = request.options.clone().fee.unwrap_or_default().thorchain; - let quote = client + let quote = self + .swap_client .get_quote( - endpoint.as_str(), from_asset.clone(), to_asset.clone(), value.to_string(), @@ -106,12 +102,12 @@ impl Swapper for ThorChain { inbound_address: quote.inbound_address.clone(), }; - let quote = SwapperQuote { + let quote = Quote { from_value: request.clone().value, to_value: to_value.to_string(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider().clone(), - routes: vec![SwapperRoute { + routes: vec![Route { input: request.from_asset.asset_id(), output: request.to_asset.asset_id(), route_data: serde_json::to_string(&route_data).unwrap_or_default(), @@ -126,7 +122,7 @@ impl Swapper for ThorChain { Ok(quote) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, provider: Arc, _data: FetchQuoteData) -> Result { + async fn fetch_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)?; @@ -145,7 +141,7 @@ impl Swapper for ThorChain { let route_data: RouteData = serde_json::from_str("e.data.routes.first().unwrap().route_data).map_err(|_| SwapperError::InvalidRoute)?; let value = quote.request.value.clone(); - let approval: Option = { + let approval: Option = { if from_asset.use_evm_router() { let from_amount: U256 = value.to_string().parse().map_err(SwapperError::from)?; check_approval_erc20( @@ -153,7 +149,7 @@ impl Swapper for ThorChain { from_asset.token_id.clone().unwrap(), route_data.router_address.clone().unwrap(), from_amount, - provider.clone(), + self.rpc_provider.clone(), &from_asset.chain.chain(), ) .await? @@ -206,11 +202,8 @@ impl Swapper for ThorChain { Ok(data) } - async fn get_swap_result(&self, chain: Chain, transaction_hash: &str, provider: Arc) -> Result { - let endpoint = provider.get_endpoint(Chain::Thorchain).map_err(SwapperError::from)?; - let client = ThorChainSwapClient::new(provider); - - let status = client.get_transaction_status(&endpoint, transaction_hash).await?; + async fn get_swap_result(&self, chain: Chain, transaction_hash: &str) -> Result { + let status = self.swap_client.get_transaction_status(transaction_hash).await?; let swap_status = status.observed_tx.swap_status(); let memo_parsed = ThorchainMemo::parse(&status.tx.memo); @@ -229,7 +222,7 @@ impl Swapper for ThorChain { _ => (None, None), }; - Ok(SwapperSwapResult { + Ok(SwapResult { status: swap_status, from_chain: chain, from_tx_hash: transaction_hash.to_string(), @@ -242,16 +235,17 @@ impl Swapper for ThorChain { #[cfg(all(test, feature = "swap_integration_tests"))] mod swap_integration_tests { use super::*; - use crate::{network::alien_provider::NativeProvider, swapper::testkit::mock_quote}; + use crate::{alien::reqwest_provider::NativeProvider, testkit::mock_quote}; + use std::sync::Arc; #[tokio::test] async fn test_thorchain_swap_trx_to_bnb() -> Result<(), Box> { - let swapper = ThorChain::default(); let provider = Arc::new(NativeProvider::default()); + let swapper = ThorChain::new(provider.clone()); let request = mock_quote(Chain::Tron.as_asset_id(), Chain::SmartChain.as_asset_id()); - let quote = swapper.fetch_quote(&request, provider.clone()).await?; + let quote = swapper.fetch_quote(&request).await?; println!("quote: {:#?}", quote); diff --git a/gemstone/src/swapper/uniswap/deadline.rs b/crates/swapper/src/uniswap/deadline.rs similarity index 100% rename from gemstone/src/swapper/uniswap/deadline.rs rename to crates/swapper/src/uniswap/deadline.rs diff --git a/crates/swapper/src/uniswap/default.rs b/crates/swapper/src/uniswap/default.rs new file mode 100644 index 000000000..42e7e13dc --- /dev/null +++ b/crates/swapper/src/uniswap/default.rs @@ -0,0 +1,51 @@ +use super::{universal_router, v3::UniswapV3, v4::UniswapV4}; +use crate::{Swapper, alien::RpcProvider}; +use std::sync::Arc; + +pub fn new_uniswap_v3(rpc_provider: Arc) -> UniswapV3 { + universal_router::new_uniswap_v3(rpc_provider) +} + +pub fn new_pancakeswap(rpc_provider: Arc) -> UniswapV3 { + universal_router::new_pancakeswap(rpc_provider) +} + +pub fn new_aerodrome(rpc_provider: Arc) -> UniswapV3 { + universal_router::new_aerodrome(rpc_provider) +} + +pub fn new_oku(rpc_provider: Arc) -> UniswapV3 { + universal_router::new_oku(rpc_provider) +} + +pub fn new_wagmi(rpc_provider: Arc) -> UniswapV3 { + universal_router::new_wagmi(rpc_provider) +} + +pub fn new_uniswap_v4(rpc_provider: Arc) -> UniswapV4 { + universal_router::new_uniswap_v4(rpc_provider) +} + +pub fn boxed_uniswap_v3(rpc_provider: Arc) -> Box { + Box::new(new_uniswap_v3(rpc_provider)) +} + +pub fn boxed_pancakeswap(rpc_provider: Arc) -> Box { + Box::new(new_pancakeswap(rpc_provider)) +} + +pub fn boxed_aerodrome(rpc_provider: Arc) -> Box { + Box::new(new_aerodrome(rpc_provider)) +} + +pub fn boxed_oku(rpc_provider: Arc) -> Box { + Box::new(new_oku(rpc_provider)) +} + +pub fn boxed_wagmi(rpc_provider: Arc) -> Box { + Box::new(new_wagmi(rpc_provider)) +} + +pub fn boxed_uniswap_v4(rpc_provider: Arc) -> Box { + Box::new(new_uniswap_v4(rpc_provider)) +} diff --git a/gemstone/src/swapper/uniswap/fee_token.rs b/crates/swapper/src/uniswap/fee_token.rs similarity index 97% rename from gemstone/src/swapper/uniswap/fee_token.rs rename to crates/swapper/src/uniswap/fee_token.rs index ce6cdb6c8..ca577485f 100644 --- a/gemstone/src/swapper/uniswap/fee_token.rs +++ b/crates/swapper/src/uniswap/fee_token.rs @@ -1,4 +1,4 @@ -use crate::swapper::SwapperMode; +use crate::SwapperMode; use alloy_primitives::Address; use gem_evm::uniswap::path::BasePair; use std::collections::HashSet; @@ -31,7 +31,6 @@ pub fn get_fee_token(mode: &SwapperMode, base_pair: Option<&BasePair>, input: &A #[cfg(test)] mod tests { use super::*; - use crate::swapper::SwapperMode; use alloy_primitives::address; use gem_evm::uniswap::path::get_base_pair; use primitives::EVMChain; diff --git a/gemstone/src/swapper/uniswap/mod.rs b/crates/swapper/src/uniswap/mod.rs similarity index 87% rename from gemstone/src/swapper/uniswap/mod.rs rename to crates/swapper/src/uniswap/mod.rs index 32f4a792c..d7cb30def 100644 --- a/gemstone/src/swapper/uniswap/mod.rs +++ b/crates/swapper/src/uniswap/mod.rs @@ -3,6 +3,7 @@ mod fee_token; mod quote_result; mod swap_route; +pub mod default; pub mod universal_router; pub mod v3; pub mod v4; diff --git a/gemstone/src/swapper/uniswap/quote_result.rs b/crates/swapper/src/uniswap/quote_result.rs similarity index 92% rename from gemstone/src/swapper/uniswap/quote_result.rs rename to crates/swapper/src/uniswap/quote_result.rs index 143c29552..46fafb57e 100644 --- a/gemstone/src/swapper/uniswap/quote_result.rs +++ b/crates/swapper/src/uniswap/quote_result.rs @@ -1,6 +1,6 @@ -use crate::network::{JsonRpcError, JsonRpcResponse, JsonRpcResult, JsonRpcResults}; -use crate::swapper::SwapperError; +use crate::SwapperError; use alloy_primitives::U256; +use gem_jsonrpc::types::{JsonRpcError, JsonRpcResponse, JsonRpcResult, JsonRpcResults}; #[derive(Debug)] pub struct QuoteResult { diff --git a/gemstone/src/swapper/uniswap/swap_route.rs b/crates/swapper/src/uniswap/swap_route.rs similarity index 91% rename from gemstone/src/swapper/uniswap/swap_route.rs rename to crates/swapper/src/uniswap/swap_route.rs index 9185bad2d..10c175687 100644 --- a/gemstone/src/swapper/uniswap/swap_route.rs +++ b/crates/swapper/src/uniswap/swap_route.rs @@ -1,4 +1,4 @@ -use crate::swapper::SwapperRoute; +use crate::Route; use alloy_primitives::Address; use gem_evm::uniswap::path::BasePair; use primitives::AssetId; @@ -29,17 +29,17 @@ pub fn build_swap_route( token_out: &AssetId, route_data: &RouteData, gas_estimate: Option, -) -> Vec { +) -> Vec { let data = serde_json::to_string(route_data).unwrap(); if let Some(intermediary) = intermediary { vec![ - SwapperRoute { + Route { input: token_in.clone(), output: intermediary.clone(), route_data: data.clone(), gas_limit: gas_estimate.clone(), }, - SwapperRoute { + Route { input: intermediary.clone(), output: token_out.clone(), route_data: data, @@ -47,7 +47,7 @@ pub fn build_swap_route( }, ] } else { - vec![SwapperRoute { + vec![Route { input: token_in.clone(), output: token_out.clone(), route_data: data, diff --git a/gemstone/src/swapper/uniswap/universal_router/aerodrome.rs b/crates/swapper/src/uniswap/universal_router/aerodrome.rs similarity index 75% rename from gemstone/src/swapper/uniswap/universal_router/aerodrome.rs rename to crates/swapper/src/uniswap/universal_router/aerodrome.rs index eb42a4304..35d5824cf 100644 --- a/gemstone/src/swapper/uniswap/universal_router/aerodrome.rs +++ b/crates/swapper/src/uniswap/universal_router/aerodrome.rs @@ -1,4 +1,4 @@ -use crate::swapper::{SwapperProvider, SwapperProviderType, uniswap::v3::UniversalRouterProvider}; +use crate::{ProviderType, SwapperProvider, uniswap::v3::UniversalRouterProvider}; use gem_evm::uniswap::{ FeeTier, deployment::v3::{V3Deployment, get_aerodrome_router_deployment_by_chain}, @@ -7,19 +7,19 @@ use primitives::Chain; #[derive(Debug)] pub struct AerodromeUniversalRouter { - pub provider: SwapperProviderType, + pub provider: ProviderType, } impl Default for AerodromeUniversalRouter { fn default() -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::Aerodrome), + provider: ProviderType::new(SwapperProvider::Aerodrome), } } } impl UniversalRouterProvider for AerodromeUniversalRouter { - fn provider(&self) -> &SwapperProviderType { + fn provider(&self) -> &ProviderType { &self.provider } diff --git a/crates/swapper/src/uniswap/universal_router/mod.rs b/crates/swapper/src/uniswap/universal_router/mod.rs new file mode 100644 index 000000000..13c2c8bd1 --- /dev/null +++ b/crates/swapper/src/uniswap/universal_router/mod.rs @@ -0,0 +1,41 @@ +mod aerodrome; +mod oku; +mod pancakeswap; +mod uniswap_v3; +mod wagmi; + +use crate::{ + alien::RpcProvider, + uniswap::{v3::UniswapV3, v4::UniswapV4}, +}; +use std::sync::Arc; + +type UniV3Router = uniswap_v3::UniswapUniversalRouter; +type PancakeRouter = pancakeswap::PancakeSwapUniversalRouter; +type AerodromeRouter = aerodrome::AerodromeUniversalRouter; +type OkuRouter = oku::OkuUniversalRouter; +type WagmiRouter = wagmi::WagmiUniversalRouter; + +pub fn new_uniswap_v3(rpc_provider: Arc) -> UniswapV3 { + UniswapV3::new(Box::new(UniV3Router::default()), rpc_provider) +} + +pub fn new_pancakeswap(rpc_provider: Arc) -> UniswapV3 { + UniswapV3::new(Box::new(PancakeRouter::default()), rpc_provider) +} + +pub fn new_aerodrome(rpc_provider: Arc) -> UniswapV3 { + UniswapV3::new(Box::new(AerodromeRouter::default()), rpc_provider) +} + +pub fn new_oku(rpc_provider: Arc) -> UniswapV3 { + UniswapV3::new(Box::new(OkuRouter::default()), rpc_provider) +} + +pub fn new_wagmi(rpc_provider: Arc) -> UniswapV3 { + UniswapV3::new(Box::new(WagmiRouter::default()), rpc_provider) +} + +pub fn new_uniswap_v4(rpc_provider: Arc) -> UniswapV4 { + UniswapV4::new(rpc_provider) +} diff --git a/gemstone/src/swapper/uniswap/universal_router/oku.rs b/crates/swapper/src/uniswap/universal_router/oku.rs similarity index 72% rename from gemstone/src/swapper/uniswap/universal_router/oku.rs rename to crates/swapper/src/uniswap/universal_router/oku.rs index bff14a6a7..ca2418a50 100644 --- a/gemstone/src/swapper/uniswap/universal_router/oku.rs +++ b/crates/swapper/src/uniswap/universal_router/oku.rs @@ -1,4 +1,4 @@ -use crate::swapper::{SwapperProvider, SwapperProviderType, uniswap::v3::UniversalRouterProvider}; +use crate::{ProviderType, SwapperProvider, uniswap::v3::UniversalRouterProvider}; use gem_evm::uniswap::{ FeeTier, deployment::v3::{V3Deployment, get_oku_deployment_by_chain}, @@ -7,19 +7,19 @@ use primitives::Chain; #[derive(Debug)] pub struct OkuUniversalRouter { - pub provider: SwapperProviderType, + pub provider: ProviderType, } impl Default for OkuUniversalRouter { fn default() -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::Oku), + provider: ProviderType::new(SwapperProvider::Oku), } } } impl UniversalRouterProvider for OkuUniversalRouter { - fn provider(&self) -> &SwapperProviderType { + fn provider(&self) -> &ProviderType { &self.provider } diff --git a/gemstone/src/swapper/uniswap/universal_router/pancakeswap.rs b/crates/swapper/src/uniswap/universal_router/pancakeswap.rs similarity index 73% rename from gemstone/src/swapper/uniswap/universal_router/pancakeswap.rs rename to crates/swapper/src/uniswap/universal_router/pancakeswap.rs index 0c9c694bb..2d325a8b9 100644 --- a/gemstone/src/swapper/uniswap/universal_router/pancakeswap.rs +++ b/crates/swapper/src/uniswap/universal_router/pancakeswap.rs @@ -1,4 +1,4 @@ -use crate::swapper::{SwapperProvider, SwapperProviderType, uniswap::v3::UniversalRouterProvider}; +use crate::{ProviderType, SwapperProvider, uniswap::v3::UniversalRouterProvider}; use gem_evm::uniswap::{ FeeTier, deployment::v3::{V3Deployment, get_pancakeswap_router_deployment_by_chain}, @@ -7,19 +7,19 @@ use primitives::Chain; #[derive(Debug)] pub struct PancakeSwapUniversalRouter { - pub provider: SwapperProviderType, + pub provider: ProviderType, } impl Default for PancakeSwapUniversalRouter { fn default() -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::PancakeswapV3), + provider: ProviderType::new(SwapperProvider::PancakeswapV3), } } } impl UniversalRouterProvider for PancakeSwapUniversalRouter { - fn provider(&self) -> &SwapperProviderType { + fn provider(&self) -> &ProviderType { &self.provider } diff --git a/gemstone/src/swapper/uniswap/universal_router/uniswap_v3.rs b/crates/swapper/src/uniswap/universal_router/uniswap_v3.rs similarity index 72% rename from gemstone/src/swapper/uniswap/universal_router/uniswap_v3.rs rename to crates/swapper/src/uniswap/universal_router/uniswap_v3.rs index 3aeefae5a..e73033df0 100644 --- a/gemstone/src/swapper/uniswap/universal_router/uniswap_v3.rs +++ b/crates/swapper/src/uniswap/universal_router/uniswap_v3.rs @@ -1,4 +1,4 @@ -use crate::swapper::{SwapperProvider, SwapperProviderType, uniswap::v3::UniversalRouterProvider}; +use crate::{ProviderType, SwapperProvider, uniswap::v3::UniversalRouterProvider}; use gem_evm::uniswap::{ FeeTier, deployment::v3::{V3Deployment, get_uniswap_router_deployment_by_chain}, @@ -7,19 +7,19 @@ use primitives::Chain; #[derive(Debug)] pub struct UniswapUniversalRouter { - pub provider: SwapperProviderType, + pub provider: ProviderType, } impl Default for UniswapUniversalRouter { fn default() -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::UniswapV3), + provider: ProviderType::new(SwapperProvider::UniswapV3), } } } impl UniversalRouterProvider for UniswapUniversalRouter { - fn provider(&self) -> &SwapperProviderType { + fn provider(&self) -> &ProviderType { &self.provider } diff --git a/gemstone/src/swapper/uniswap/universal_router/wagmi.rs b/crates/swapper/src/uniswap/universal_router/wagmi.rs similarity index 72% rename from gemstone/src/swapper/uniswap/universal_router/wagmi.rs rename to crates/swapper/src/uniswap/universal_router/wagmi.rs index e2f83d681..9b6db63f4 100644 --- a/gemstone/src/swapper/uniswap/universal_router/wagmi.rs +++ b/crates/swapper/src/uniswap/universal_router/wagmi.rs @@ -1,4 +1,4 @@ -use crate::swapper::{SwapperProvider, SwapperProviderType, uniswap::v3::UniversalRouterProvider}; +use crate::{ProviderType, SwapperProvider, uniswap::v3::UniversalRouterProvider}; use gem_evm::uniswap::{ FeeTier, deployment::v3::{V3Deployment, get_wagmi_router_deployment_by_chain}, @@ -7,19 +7,19 @@ use primitives::Chain; #[derive(Debug)] pub struct WagmiUniversalRouter { - pub provider: SwapperProviderType, + pub provider: ProviderType, } impl Default for WagmiUniversalRouter { fn default() -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::Wagmi), + provider: ProviderType::new(SwapperProvider::Wagmi), } } } impl UniversalRouterProvider for WagmiUniversalRouter { - fn provider(&self) -> &SwapperProviderType { + fn provider(&self) -> &ProviderType { &self.provider } diff --git a/gemstone/src/swapper/uniswap/v3/commands.rs b/crates/swapper/src/uniswap/v3/commands.rs similarity index 94% rename from gemstone/src/swapper/uniswap/v3/commands.rs rename to crates/swapper/src/uniswap/v3/commands.rs index e5efc591e..6232e7a7f 100644 --- a/gemstone/src/swapper/uniswap/v3/commands.rs +++ b/crates/swapper/src/uniswap/v3/commands.rs @@ -1,11 +1,11 @@ -use crate::swapper::{SwapperError, SwapperMode, eth_address, models::*, slippage::apply_slippage_in_bp}; +use crate::{SwapperError, SwapperMode, eth_address, models::*, slippage::apply_slippage_in_bp}; use gem_evm::uniswap::command::{ADDRESS_THIS, PayPortion, Permit2Permit, Sweep, Transfer, UniversalRouterCommand, UnwrapWeth, V3SwapExactIn, WrapEth}; use alloy_primitives::{Address, Bytes, U256}; use std::str::FromStr; pub fn build_commands( - request: &SwapperQuoteRequest, + request: &QuoteRequest, token_in: &Address, token_out: &Address, amount_in: U256, @@ -126,8 +126,8 @@ pub fn build_commands( mod tests { use super::*; use crate::{ - config::swap_config::{SwapReferralFee, SwapReferralFees}, - swapper::permit2_data::*, + config::{ReferralFee, ReferralFees}, + permit2_data::*, }; use alloy_primitives::aliases::U256; use gem_evm::uniswap::{FeeTier, path::build_direct_pair}; @@ -135,7 +135,7 @@ mod tests { #[test] fn test_build_commands_eth_to_token() { - let mut request = SwapperQuoteRequest { + let mut request = QuoteRequest { // ETH -> USDC from_asset: AssetId::from(Chain::Ethereum, None).into(), to_asset: AssetId::from(Chain::Ethereum, Some("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".into())).into(), @@ -143,7 +143,7 @@ mod tests { destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), value: "10000000000000000".into(), mode: SwapperMode::ExactIn, - options: SwapperOptions::default(), + options: Options::default(), }; let token_in = eth_address::parse_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); @@ -159,9 +159,9 @@ mod tests { assert!(matches!(commands[0], UniversalRouterCommand::WRAP_ETH(_))); assert!(matches!(commands[1], UniversalRouterCommand::V3_SWAP_EXACT_IN(_))); - let options = SwapperOptions { + let options = Options { slippage: 100.into(), - fee: Some(SwapReferralFees::evm(SwapReferralFee { + fee: Some(ReferralFees::evm(ReferralFee { bps: 25, address: "0x3d83ec320541ae96c4c91e9202643870458fb290".into(), })), @@ -181,7 +181,7 @@ mod tests { #[test] fn test_build_commands_usdc_to_usdt() { - let request = SwapperQuoteRequest { + let request = QuoteRequest { // USDC -> USDT from_asset: AssetId::from(Chain::Optimism, Some("0x0b2c639c533813f4aa9d7837caf62653d097ff85".into())).into(), to_asset: AssetId::from(Chain::Optimism, Some("0x94b008aa00579c1307b0ef2c499ad98a8ce58e58".into())).into(), @@ -189,7 +189,7 @@ mod tests { destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), value: "6500000".into(), mode: SwapperMode::ExactIn, - options: SwapperOptions::default(), + options: Options::default(), }; let token_in = eth_address::parse_str(request.from_asset.asset_id().token_id.as_ref().unwrap()).unwrap(); @@ -234,7 +234,7 @@ mod tests { #[test] fn test_build_commands_usdc_to_aave() { - let request = SwapperQuoteRequest { + let request = QuoteRequest { // USDC -> AAVE from_asset: AssetId::from(Chain::Optimism, Some("0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85".into())).into(), to_asset: AssetId::from(Chain::Optimism, Some("0x76fb31fb4af56892a25e32cfc43de717950c9278".into())).into(), @@ -242,9 +242,9 @@ mod tests { destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), value: "5064985".into(), mode: SwapperMode::ExactIn, - options: SwapperOptions { + options: Options { slippage: 100.into(), - fee: Some(SwapReferralFees::evm(SwapReferralFee { + fee: Some(ReferralFees::evm(ReferralFee { bps: 25, address: "0x3d83ec320541ae96c4c91e9202643870458fb290".into(), })), @@ -277,7 +277,7 @@ mod tests { #[test] fn test_build_commands_usdce_to_eth() { - let request = SwapperQuoteRequest { + let request = QuoteRequest { // USDCE -> ETH from_asset: AssetId::from(Chain::Optimism, Some("0x7F5c764cBc14f9669B88837ca1490cCa17c31607".into())).into(), to_asset: AssetId::from(Chain::Ethereum, None).into(), @@ -285,9 +285,9 @@ mod tests { destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), value: "10000000".into(), mode: SwapperMode::ExactIn, - options: SwapperOptions { + options: Options { slippage: 100.into(), - fee: Some(SwapReferralFees::evm(SwapReferralFee { + fee: Some(ReferralFees::evm(ReferralFee { bps: 25, address: "0x3d83ec320541ae96c4c91e9202643870458fb290".into(), })), @@ -340,7 +340,7 @@ mod tests { #[test] fn test_build_commands_eth_to_uni_with_input_fee() { // Replicate https://optimistic.etherscan.io/tx/0x18277deea3e273a7fb9abc985269dcdabe3d34c2b604fbd82dcd0a5a5204f72c - let request = SwapperQuoteRequest { + let request = QuoteRequest { // ETH -> UNI from_asset: AssetId::from(Chain::Optimism, None).into(), to_asset: AssetId::from(Chain::Optimism, Some("0x6fd9d7ad17242c41f7131d257212c54a0e816691".into())).into(), @@ -348,9 +348,9 @@ mod tests { destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), value: "1000000000000000".into(), mode: SwapperMode::ExactIn, - options: SwapperOptions { + options: Options { slippage: 100.into(), - fee: Some(SwapReferralFees::evm(SwapReferralFee { + fee: Some(ReferralFees::evm(ReferralFee { bps: 25, address: "0x3d83ec320541ae96c4c91e9202643870458fb290".into(), })), diff --git a/gemstone/src/swapper/uniswap/v3/mod.rs b/crates/swapper/src/uniswap/v3/mod.rs similarity index 82% rename from gemstone/src/swapper/uniswap/v3/mod.rs rename to crates/swapper/src/uniswap/v3/mod.rs index d4d0e81fe..06165a2bf 100644 --- a/gemstone/src/swapper/uniswap/v3/mod.rs +++ b/crates/swapper/src/uniswap/v3/mod.rs @@ -5,13 +5,13 @@ mod quoter_v2; pub mod provider; pub use provider::UniswapV3; -use crate::swapper::SwapperProviderType; +use crate::ProviderType; use gem_evm::uniswap::{FeeTier, deployment::v3::V3Deployment}; use primitives::Chain; use std::fmt::Debug; pub trait UniversalRouterProvider: Send + Sync + Debug { - fn provider(&self) -> &SwapperProviderType; + fn provider(&self) -> &ProviderType; fn get_tiers(&self) -> Vec; fn get_deployment_by_chain(&self, chain: &Chain) -> Option; } diff --git a/gemstone/src/swapper/uniswap/v3/path.rs b/crates/swapper/src/uniswap/v3/path.rs similarity index 92% rename from gemstone/src/swapper/uniswap/v3/path.rs rename to crates/swapper/src/uniswap/v3/path.rs index c6d5b03df..a5ea08ef1 100644 --- a/gemstone/src/swapper/uniswap/v3/path.rs +++ b/crates/swapper/src/uniswap/v3/path.rs @@ -4,8 +4,8 @@ use gem_evm::uniswap::{ path::{BasePair, TokenPair, build_direct_pair, build_pairs}, }; -use crate::swapper::{ - SwapperError, SwapperRoute, eth_address, +use crate::{ + Route, SwapperError, eth_address, uniswap::swap_route::{RouteData, get_intermediaries}, }; @@ -38,7 +38,7 @@ pub fn build_paths(token_in: &Address, token_out: &Address, fee_tiers: &[FeeTier paths } -pub fn build_paths_with_routes(routes: &[SwapperRoute]) -> Result { +pub fn build_paths_with_routes(routes: &[Route]) -> Result { if routes.is_empty() { return Err(SwapperError::InvalidRoute); } diff --git a/gemstone/src/swapper/uniswap/v3/provider.rs b/crates/swapper/src/uniswap/v3/provider.rs similarity index 73% rename from gemstone/src/swapper/uniswap/v3/provider.rs rename to crates/swapper/src/uniswap/v3/provider.rs index 429317c9e..ac9d34af2 100644 --- a/gemstone/src/swapper/uniswap/v3/provider.rs +++ b/crates/swapper/src/uniswap/v3/provider.rs @@ -1,52 +1,55 @@ use crate::{ - models::GemApprovalData, - network::{AlienProvider, jsonrpc_client_with_chain}, - swapper::{ - Swapper, SwapperError, SwapperQuoteData, - approval::{check_approval_erc20, check_approval_permit2}, - eth_address, - models::*, - slippage::apply_slippage_in_bp, - uniswap::{ - deadline::get_sig_deadline, - fee_token::get_fee_token, - quote_result::get_best_quote, - swap_route::{RouteData, build_swap_route}, - }, + FetchQuoteData, Permit2ApprovalData, ProviderData, ProviderType, Quote, QuoteRequest, Swapper, SwapperError, SwapperQuoteData, + alien::{RpcClient, RpcProvider}, + approval::{check_approval_erc20_with_client, check_approval_permit2_with_client}, + eth_address, + models::*, + slippage::apply_slippage_in_bp, + uniswap::{ + deadline::get_sig_deadline, + fee_token::get_fee_token, + quote_result::get_best_quote, + swap_route::{RouteData, build_swap_route}, }, }; +use alloy_primitives::{Address, Bytes, U256, hex::encode_prefixed as HexEncode}; +use async_trait::async_trait; use gem_evm::{ jsonrpc::EthereumRpc, uniswap::{command::encode_commands, path::get_base_pair}, }; -use primitives::{AssetId, Chain, EVMChain}; - -use alloy_primitives::{Address, Bytes, U256, hex::encode_prefixed as HexEncode}; -use async_trait::async_trait; -use std::{fmt::Debug, str::FromStr, sync::Arc}; +use gem_jsonrpc::client::JsonRpcClient; +use primitives::{AssetId, Chain, EVMChain, swap::ApprovalData}; +use std::{fmt, str::FromStr, sync::Arc}; use super::{DEFAULT_SWAP_GAS_LIMIT, UniversalRouterProvider, commands::build_commands, path::build_paths_with_routes}; -#[derive(Debug)] pub struct UniswapV3 { provider: Box, + rpc_provider: Arc, } impl UniswapV3 { - pub fn new(provider: Box) -> Self { - Self { provider } + pub fn new(provider: Box, rpc_provider: Arc) -> Self { + Self { provider, rpc_provider } } pub fn support_chain(&self, chain: &Chain) -> bool { self.provider.get_deployment_by_chain(chain).is_some() } + fn client_for(&self, chain: Chain) -> Result, SwapperError> { + let endpoint = self.rpc_provider.get_endpoint(chain).map_err(SwapperError::from)?; + let client = RpcClient::new(endpoint, self.rpc_provider.clone()); + Ok(JsonRpcClient::new(client)) + } + fn get_asset_address(asset_id: &str, evm_chain: EVMChain) -> Result { let asset_id = AssetId::new(asset_id).ok_or(SwapperError::NotSupportedAsset)?; eth_address::normalize_weth_address(&asset_id, evm_chain) } - fn parse_request(request: &SwapperQuoteRequest) -> Result<(EVMChain, Address, Address, U256), SwapperError> { + fn parse_request(request: &QuoteRequest) -> Result<(EVMChain, Address, Address, U256), SwapperError> { let evm_chain = EVMChain::from_chain(request.from_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; let token_in = Self::get_asset_address(&request.from_asset.id, evm_chain)?; let token_out = Self::get_asset_address(&request.to_asset.id, evm_chain)?; @@ -57,45 +60,49 @@ impl UniswapV3 { async fn check_erc20_approval( &self, + client: &JsonRpcClient, wallet_address: Address, token: &str, amount: U256, chain: &Chain, - provider: Arc, ) -> Result { let deployment = self.provider.get_deployment_by_chain(chain).ok_or(SwapperError::NotSupportedChain)?; let spender = deployment.permit2.to_string(); - // Check token allowance, spender is permit2 or universal router - check_approval_erc20(wallet_address.to_string(), token.to_string(), spender, amount, provider, chain).await + check_approval_erc20_with_client(wallet_address.to_string(), token.to_string(), spender, amount, client).await } async fn check_permit2_approval( &self, + client: &JsonRpcClient, wallet_address: Address, token: &str, amount: U256, chain: &Chain, - provider: Arc, ) -> Result, SwapperError> { let deployment = self.provider.get_deployment_by_chain(chain).ok_or(SwapperError::NotSupportedChain)?; - Ok(check_approval_permit2( + Ok(check_approval_permit2_with_client( deployment.permit2, wallet_address.to_string(), token.to_string(), deployment.universal_router.to_string(), amount, - provider.clone(), - chain, + client, ) .await? .permit2_data()) } } +impl fmt::Debug for UniswapV3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UniswapV3").finish() + } +} + #[async_trait] impl Swapper for UniswapV3 { - fn provider(&self) -> &SwapperProviderType { + fn provider(&self) -> &ProviderType { self.provider.provider() } @@ -107,59 +114,47 @@ impl Swapper for UniswapV3 { .collect() } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result { + async fn fetch_quote(&self, request: &QuoteRequest) -> Result { let from_chain = request.from_asset.chain(); let to_chain = request.to_asset.chain(); - // Check deployment and weth contract let deployment = self.provider.get_deployment_by_chain(&from_chain).ok_or(SwapperError::NotSupportedChain)?; let (evm_chain, token_in, token_out, from_value) = Self::parse_request(request)?; _ = evm_chain.weth_contract().ok_or(SwapperError::NotSupportedChain)?; + let client = Arc::new(self.client_for(from_chain)?); + let fee_tiers = self.provider.get_tiers(); let base_pair = get_base_pair(&evm_chain, true).ok_or(SwapperError::ComputeQuoteError("base pair not found".into()))?; let fee_preference = get_fee_token(&request.mode, Some(&base_pair), &token_in, &token_out); let fee_bps = request.options.clone().fee.unwrap_or_default().evm.bps; - // If fees are taken from input token, we need to use remaining amount as quote amount let quote_amount_in = if fee_preference.is_input_token && fee_bps > 0 { apply_slippage_in_bp(&from_value, fee_bps) } else { from_value }; - // Build paths for QuoterV2 - // [ - // [direct_fee_tier1, ..., ..., ... ], - // [weth_hop_fee_tier1, ..., ..., ... ], - // [usdc_hop_fee_tier1, ..., ..., ... ], - // [...], - // ] let paths_array = super::path::build_paths(&token_in, &token_out, &fee_tiers, &base_pair); - let client = jsonrpc_client_with_chain(provider.clone(), from_chain); let requests: Vec<_> = paths_array .iter() .map(|paths| { + let client = client.clone(); let calls: Vec = paths .iter() .map(|path| super::quoter_v2::build_quoter_request(&request.mode, &request.wallet_address, deployment.quoter_v2, quote_amount_in, &path.1)) .collect(); - - // Use the more convenient batch_call_requests method - client.batch_call_requests(calls) + async move { client.batch_call_requests(calls).await } }) .collect(); - // fire batch requests in parallel let batch_results = futures::future::join_all(requests).await; let quote_result = get_best_quote(&batch_results, super::quoter_v2::decode_quoter_response)?; let to_value = if fee_preference.is_input_token { - // fees are taken from input token quote_result.amount_out } else { - // fees are taken from output token apply_slippage_in_bp("e_result.amount_out, fee_bps) }; let to_min_value = apply_slippage_in_bp(&to_value, request.options.slippage.bps); @@ -168,14 +163,11 @@ impl Swapper for UniswapV3 { let batch_idx = quote_result.batch_idx; let gas_estimate = quote_result.gas_estimate; - // construct routes let fee_tier: u32 = fee_tiers[fee_tier_idx % fee_tiers.len()] as u32; let asset_id_in = AssetId::from(from_chain, Some(token_in.to_checksum(None))); let asset_id_out = AssetId::from(to_chain, Some(token_out.to_checksum(None))); let asset_id_intermediary: Option = match batch_idx { - // direct route 0 => None, - // 2 hop route with intermediary token _ => { let first_token_out = &paths_array[batch_idx][0].0[0].token_out; Some(AssetId::from(to_chain, Some(first_token_out.to_checksum(None)))) @@ -187,10 +179,10 @@ impl Swapper for UniswapV3 { }; let routes = build_swap_route(&asset_id_in, asset_id_intermediary.as_ref(), &asset_id_out, &route_data, gas_estimate); - Ok(SwapperQuote { + Ok(Quote { from_value: request.value.clone(), to_value: to_value.to_string(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider().clone(), routes: routes.clone(), slippage_bps: request.options.slippage.bps, @@ -200,23 +192,26 @@ impl Swapper for UniswapV3 { }) } - async fn fetch_permit2_for_quote(&self, quote: &SwapperQuote, provider: Arc) -> Result, SwapperError> { + async fn fetch_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); } + let client = self.client_for(from_asset.chain)?; let wallet_address = eth_address::parse_str("e.request.wallet_address)?; let (_, token_in, _, amount_in) = Self::parse_request("e.request)?; - self.check_permit2_approval(wallet_address, &token_in.to_checksum(None), amount_in, &from_asset.chain, provider) + self.check_permit2_approval(&client, wallet_address, &token_in.to_checksum(None), amount_in, &from_asset.chain) .await } - async fn fetch_quote_data(&self, quote: &SwapperQuote, provider: Arc, data: FetchQuoteData) -> Result { + async fn fetch_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)?; let deployment = self.provider.get_deployment_by_chain(&from_chain).ok_or(SwapperError::NotSupportedChain)?; + let client = self.client_for(from_chain)?; + let route_data: RouteData = serde_json::from_str("e.data.routes.first().unwrap().route_data).map_err(|_| SwapperError::InvalidRoute)?; let to_amount = U256::from_str(&route_data.min_amount_out).map_err(SwapperError::from)?; @@ -224,10 +219,10 @@ impl Swapper for UniswapV3 { let permit = data.permit2_data().map(|data| data.into()); let mut gas_limit: Option = None; - let approval: Option = if quote.request.from_asset.is_native() { + let approval: Option = if quote.request.from_asset.is_native() { None } else { - self.check_erc20_approval(wallet_address, &token_in.to_checksum(None), amount_in, &from_chain, provider) + self.check_erc20_approval(&client, wallet_address, &token_in.to_checksum(None), amount_in, &from_chain) .await? .approval_data() }; diff --git a/gemstone/src/swapper/uniswap/v3/quoter_v2.rs b/crates/swapper/src/uniswap/v3/quoter_v2.rs similarity index 96% rename from gemstone/src/swapper/uniswap/v3/quoter_v2.rs rename to crates/swapper/src/uniswap/v3/quoter_v2.rs index d6e2eb367..5d1677396 100644 --- a/gemstone/src/swapper/uniswap/v3/quoter_v2.rs +++ b/crates/swapper/src/uniswap/v3/quoter_v2.rs @@ -4,11 +4,9 @@ use gem_evm::{ jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, uniswap::contracts::v3::IQuoterV2, }; +use gem_jsonrpc::types::JsonRpcResponse; -use crate::{ - network::JsonRpcResponse, - swapper::{SwapperError, SwapperMode}, -}; +use crate::{SwapperError, SwapperMode}; pub fn build_quoter_request(mode: &SwapperMode, wallet_address: &str, quoter_v2: &str, amount_in: U256, path: &Bytes) -> EthereumRpc { let call_data: Vec = match mode { diff --git a/gemstone/src/swapper/uniswap/v4/commands.rs b/crates/swapper/src/uniswap/v4/commands.rs similarity index 95% rename from gemstone/src/swapper/uniswap/v4/commands.rs rename to crates/swapper/src/uniswap/v4/commands.rs index 4f1e9565e..fa0ecd51e 100644 --- a/gemstone/src/swapper/uniswap/v4/commands.rs +++ b/crates/swapper/src/uniswap/v4/commands.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use crate::swapper::{SwapperError, SwapperMode, SwapperQuoteRequest, SwapperRoute, eth_address, slippage::apply_slippage_in_bp}; +use crate::{QuoteRequest, Route, SwapperError, SwapperMode, eth_address, slippage::apply_slippage_in_bp}; use alloy_primitives::{Address, U256}; use gem_evm::uniswap::{ actions::V4Action::{SETTLE, SWAP_EXACT_IN, TAKE}, @@ -9,12 +9,12 @@ use gem_evm::uniswap::{ }; pub fn build_commands( - request: &SwapperQuoteRequest, + request: &QuoteRequest, token_in: &Address, token_out: &Address, amount_in: u128, quote_amount: u128, - swap_routes: &[SwapperRoute], + swap_routes: &[Route], permit: Option, fee_token_is_input: bool, ) -> Result, SwapperError> { @@ -98,7 +98,7 @@ fn build_v4_swap_command( token_out: &Address, amount_in: u128, amount_out_min: u128, - swap_routes: &[SwapperRoute], + swap_routes: &[Route], recipient: &Address, ) -> Result { if swap_routes.is_empty() { diff --git a/gemstone/src/swapper/uniswap/v4/mod.rs b/crates/swapper/src/uniswap/v4/mod.rs similarity index 100% rename from gemstone/src/swapper/uniswap/v4/mod.rs rename to crates/swapper/src/uniswap/v4/mod.rs diff --git a/gemstone/src/swapper/uniswap/v4/path.rs b/crates/swapper/src/uniswap/v4/path.rs similarity index 94% rename from gemstone/src/swapper/uniswap/v4/path.rs rename to crates/swapper/src/uniswap/v4/path.rs index 40efaaf91..53931ae79 100644 --- a/gemstone/src/swapper/uniswap/v4/path.rs +++ b/crates/swapper/src/uniswap/v4/path.rs @@ -5,7 +5,7 @@ use gem_evm::uniswap::{ path::TokenPair, }; -use crate::swapper::{SwapperError, SwapperRoute, eth_address, uniswap::swap_route::RouteData}; +use crate::{Route, SwapperError, eth_address, uniswap::swap_route::RouteData}; // return (currency0, currency1) fn sort_addresses(token_in: &Address, token_out: &Address) -> (Address, Address) { @@ -87,10 +87,10 @@ pub fn build_quote_exact_params( .collect() } -impl TryFrom<&SwapperRoute> for PathKey { +impl TryFrom<&Route> for PathKey { type Error = SwapperError; - fn try_from(value: &SwapperRoute) -> Result { + fn try_from(value: &Route) -> Result { let token_id = value.output.token_id.as_ref().ok_or(SwapperError::InvalidAddress(value.output.to_string()))?; let currency = eth_address::parse_str(token_id)?; diff --git a/gemstone/src/swapper/uniswap/v4/provider.rs b/crates/swapper/src/uniswap/v4/provider.rs similarity index 71% rename from gemstone/src/swapper/uniswap/v4/provider.rs rename to crates/swapper/src/uniswap/v4/provider.rs index adc7f8153..7d73967a1 100644 --- a/gemstone/src/swapper/uniswap/v4/provider.rs +++ b/crates/swapper/src/uniswap/v4/provider.rs @@ -1,24 +1,22 @@ use alloy_primitives::{Address, U256, hex::encode_prefixed as HexEncode}; use async_trait::async_trait; -use std::{collections::HashSet, str::FromStr, sync::Arc, vec}; +use std::{collections::HashSet, fmt, str::FromStr, sync::Arc, vec}; use crate::{ - models::GemApprovalData, - network::{AlienProvider, jsonrpc_client_with_chain}, - swapper::{ - FetchQuoteData, Permit2ApprovalData, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperProviderData, SwapperProviderType, SwapperQuote, - SwapperQuoteData, SwapperQuoteRequest, - approval::{check_approval_erc20, check_approval_permit2}, - eth_address, - slippage::apply_slippage_in_bp, - uniswap::{ - deadline::get_sig_deadline, - fee_token::get_fee_token, - quote_result::get_best_quote, - swap_route::{RouteData, build_swap_route, get_intermediaries}, - }, + FetchQuoteData, Permit2ApprovalData, ProviderData, ProviderType, Quote, QuoteRequest, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, + SwapperQuoteData, + alien::{RpcClient, RpcProvider}, + approval::evm::{check_approval_erc20_with_client, check_approval_permit2_with_client}, + eth_address, + slippage::apply_slippage_in_bp, + uniswap::{ + deadline::get_sig_deadline, + fee_token::get_fee_token, + quote_result::get_best_quote, + swap_route::{RouteData, build_swap_route, get_intermediaries}, }, }; +use futures::future::{BoxFuture, join_all}; use gem_evm::{ jsonrpc::EthereumRpc, uniswap::{ @@ -29,7 +27,8 @@ use gem_evm::{ path::{TokenPair, get_base_pair}, }, }; -use primitives::{AssetId, Chain, EVMChain}; +use gem_jsonrpc::client::JsonRpcClient; +use primitives::{AssetId, Chain, EVMChain, swap::ApprovalData}; use super::{ DEFAULT_SWAP_GAS_LIMIT, @@ -38,23 +37,18 @@ use super::{ quoter::{build_quote_exact_requests, build_quote_exact_single_request}, }; -#[derive(Debug)] pub struct UniswapV4 { - pub provider: SwapperProviderType, + pub provider: ProviderType, + rpc_provider: Arc, } -impl Default for UniswapV4 { - fn default() -> Self { +impl UniswapV4 { + pub fn new(rpc_provider: Arc) -> Self { Self { - provider: SwapperProviderType::new(SwapperProvider::UniswapV4), + provider: ProviderType::new(SwapperProvider::UniswapV4), + rpc_provider, } } -} - -impl UniswapV4 { - pub fn boxed() -> Box { - Box::new(Self::default()) - } fn support_chain(&self, chain: &Chain) -> bool { get_uniswap_deployment_by_chain(chain).is_some() @@ -64,6 +58,12 @@ impl UniswapV4 { vec![FeeTier::Hundred, FeeTier::FiveHundred, FeeTier::ThreeThousand, FeeTier::TenThousand] } + fn client_for(&self, chain: Chain) -> Result, SwapperError> { + let endpoint = self.rpc_provider.get_endpoint(chain).map_err(SwapperError::from)?; + let client = RpcClient::new(endpoint, self.rpc_provider.clone()); + Ok(JsonRpcClient::new(client)) + } + fn is_base_pair(token_in: &Address, token_out: &Address, evm_chain: &EVMChain) -> bool { let base_set: HashSet

= HashSet::from_iter(get_base_pair(evm_chain, false).unwrap().path_building_array()); base_set.contains(token_in) || base_set.contains(token_out) @@ -78,7 +78,7 @@ impl UniswapV4 { } } - fn parse_request(request: &SwapperQuoteRequest) -> Result<(EVMChain, Address, Address, u128), SwapperError> { + fn parse_request(request: &QuoteRequest) -> Result<(EVMChain, Address, Address, u128), SwapperError> { let evm_chain = EVMChain::from_chain(request.from_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; let token_in = Self::parse_asset_address(&request.from_asset.id, evm_chain)?; let token_out = Self::parse_asset_address(&request.to_asset.id, evm_chain)?; @@ -88,11 +88,18 @@ impl UniswapV4 { } } +impl fmt::Debug for UniswapV4 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UniswapV4").finish() + } +} + #[async_trait] impl Swapper for UniswapV4 { - fn provider(&self) -> &SwapperProviderType { + fn provider(&self) -> &ProviderType { &self.provider } + fn supported_assets(&self) -> Vec { Chain::all() .iter() @@ -100,10 +107,10 @@ impl Swapper for UniswapV4 { .map(|x| SwapperChainAsset::All(*x)) .collect() } - async fn fetch_quote(&self, request: &SwapperQuoteRequest, provider: Arc) -> Result { + + async fn fetch_quote(&self, request: &QuoteRequest) -> Result { let from_chain = request.from_asset.chain(); let to_chain = request.to_asset.chain(); - // Check deployment and weth contract let deployment = get_uniswap_deployment_by_chain(&from_chain).ok_or(SwapperError::NotSupportedChain)?; let (evm_chain, token_in, token_out, from_value) = Self::parse_request(request)?; _ = evm_chain.weth_contract().ok_or(SwapperError::NotSupportedChain)?; @@ -112,27 +119,22 @@ impl Swapper for UniswapV4 { let base_pair = get_base_pair(&evm_chain, false).ok_or(SwapperError::ComputeQuoteError("base pair not found".into()))?; let fee_preference = get_fee_token(&request.mode, Some(&base_pair), &token_in, &token_out); let fee_bps = request.options.clone().fee.unwrap_or_default().evm.bps; - // If fees are taken from input token, we need to use remaining amount as quote amount let quote_amount_in = if fee_preference.is_input_token && fee_bps > 0 { apply_slippage_in_bp(&from_value, fee_bps) } else { from_value }; - // Build PoolKeys for Quoter - // [ - // [direct_fee_tier1, ..., ..., ... ], - // [weth_hop_fee_tier1, ..., ..., ... ], - // [usdc_hop_fee_tier1, ..., ..., ... ], - // ] let pool_keys = build_pool_keys(&token_in, &token_out, &fee_tiers); - let calls: Vec = pool_keys + let client = Arc::new(self.client_for(from_chain)?); + + let mut requests: Vec> = Vec::new(); + let initial_client = Arc::clone(&client); + let direct_calls: Vec = pool_keys .iter() .map(|pool_key| build_quote_exact_single_request(&token_in, deployment.quoter, quote_amount_in, &pool_key.1)) .collect(); - let client = jsonrpc_client_with_chain(provider.clone(), from_chain); - let batch_call = client.batch_call_requests(calls); - let mut requests = vec![batch_call]; + requests.push(Box::pin(async move { initial_client.batch_call_requests(direct_calls).await })); let quote_exact_params: Vec, QuoteExactParams)>>; if !Self::is_base_pair(&token_in, &token_out, &evm_chain) { @@ -141,15 +143,15 @@ impl Swapper for UniswapV4 { build_quote_exact_requests(deployment.quoter, "e_exact_params) .iter() .for_each(|call_array| { - let batch_call = client.batch_call_requests(call_array.clone()); - requests.push(batch_call); + let client = Arc::clone(&client); + let calls = call_array.clone(); + requests.push(Box::pin(async move { client.batch_call_requests(calls).await })); }); } else { quote_exact_params = vec![]; } - // fire batch requests in parallel - let batch_results = futures::future::join_all(requests).await; + let batch_results = join_all(requests).await; let quote_result = get_best_quote(&batch_results, super::quoter::decode_quoter_response)?; @@ -158,22 +160,17 @@ impl Swapper for UniswapV4 { let gas_estimate = quote_result.gas_estimate; let to_value = if fee_preference.is_input_token { - // fees are taken from input token quote_result.amount_out } else { - // fees are taken from output tokene apply_slippage_in_bp("e_result.amount_out, fee_bps) }; let to_min_value = apply_slippage_in_bp(&to_value, request.options.slippage.bps); - // construct routes let fee_tier: u32 = fee_tiers[fee_tier_idx % fee_tiers.len()] as u32; let asset_id_in = AssetId::from(from_chain, Some(token_in.to_checksum(None))); let asset_id_out = AssetId::from(to_chain, Some(token_out.to_checksum(None))); let asset_id_intermediary: Option = match batch_idx { - // direct route 0 => None, - // 2 hop route with intermediary token _ => { let first_token_out = "e_exact_params[batch_idx][0].0[0].token_out; Some(AssetId::from(to_chain, Some(first_token_out.to_checksum(None)))) @@ -185,10 +182,10 @@ impl Swapper for UniswapV4 { }; let routes = build_swap_route(&asset_id_in, asset_id_intermediary.as_ref(), &asset_id_out, &route_data, gas_estimate); - Ok(SwapperQuote { + Ok(Quote { from_value: request.value.clone(), to_value: to_value.to_string(), - data: SwapperProviderData { + data: ProviderData { provider: self.provider().clone(), routes: routes.clone(), slippage_bps: request.options.slippage.bps, @@ -198,22 +195,22 @@ impl Swapper for UniswapV4 { }) } - async fn fetch_permit2_for_quote(&self, quote: &SwapperQuote, provider: Arc) -> Result, SwapperError> { + async fn fetch_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); } let (_, token_in, _, amount_in) = Self::parse_request("e.request)?; - let v4_deployment = get_uniswap_deployment_by_chain(&from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; + let deployment = get_uniswap_deployment_by_chain(&from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; - let permit2_data = check_approval_permit2( - v4_deployment.permit2, + let client = self.client_for(from_asset.chain)?; + let permit2_data = check_approval_permit2_with_client( + deployment.permit2, quote.request.wallet_address.clone(), token_in.to_string(), - v4_deployment.universal_router.to_string(), + deployment.universal_router.to_string(), U256::from(amount_in), - provider.clone(), - &from_asset.chain, + &client, ) .await? .permit2_data(); @@ -221,7 +218,7 @@ impl Swapper for UniswapV4 { Ok(permit2_data) } - async fn fetch_quote_data(&self, quote: &SwapperQuote, provider: Arc, data: FetchQuoteData) -> Result { + async fn fetch_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)?; @@ -229,20 +226,19 @@ impl Swapper for UniswapV4 { let route_data: RouteData = serde_json::from_str("e.data.routes.first().unwrap().route_data).map_err(|_| SwapperError::InvalidRoute)?; let to_amount = u128::from_str(&route_data.min_amount_out).map_err(SwapperError::from)?; + let client = self.client_for(from_asset.chain)?; let permit = data.permit2_data().map(|data| data.into()); let mut gas_limit: Option = None; - let approval: Option = if quote.request.from_asset.is_native() { + let approval: Option = if quote.request.from_asset.is_native() { None } else { - // Check if need to approve permit2 contract - check_approval_erc20( + check_approval_erc20_with_client( request.wallet_address.clone(), token_in.to_string(), deployment.permit2.to_string(), U256::from(amount_in), - provider, - &from_asset.chain, + &client, ) .await? .approval_data() @@ -283,24 +279,28 @@ impl Swapper for UniswapV4 { #[cfg(test)] mod tests { - use crate::swapper::{SwapperMode, SwapperOptions}; - use super::*; + use crate::{Options, SwapperMode, alien::mock::ProviderMock}; + use std::sync::Arc; #[test] fn test_is_base_pair() { - let request = SwapperQuoteRequest { + let provider = Arc::new(ProviderMock::new("{}".to_string())); + let swapper = UniswapV4::new(provider); + let request = QuoteRequest { from_asset: AssetId::from(Chain::SmartChain, Some("0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82".to_string())).into(), to_asset: AssetId::from_chain(Chain::SmartChain).into(), wallet_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), value: "40000000000000000".into(), // 0.04 Cake mode: SwapperMode::ExactIn, - options: SwapperOptions::default(), + options: Options::default(), }; let (evm_chain, token_in, token_out, _) = UniswapV4::parse_request(&request).unwrap(); assert!(UniswapV4::is_base_pair(&token_in, &token_out, &evm_chain)); + // Ensure provider field is used to avoid warnings + assert_eq!(swapper.provider.id, SwapperProvider::UniswapV4); } } diff --git a/gemstone/src/swapper/uniswap/v4/quoter.rs b/crates/swapper/src/uniswap/v4/quoter.rs similarity index 96% rename from gemstone/src/swapper/uniswap/v4/quoter.rs rename to crates/swapper/src/uniswap/v4/quoter.rs index 0f0e72b19..53505f5e4 100644 --- a/gemstone/src/swapper/uniswap/v4/quoter.rs +++ b/crates/swapper/src/uniswap/v4/quoter.rs @@ -1,4 +1,4 @@ -use crate::{network::JsonRpcResponse, swapper::SwapperError}; +use crate::SwapperError; use alloy_primitives::{Address, Bytes, U256, hex::decode as HexDecode}; use alloy_sol_types::SolCall; use gem_evm::{ @@ -8,6 +8,7 @@ use gem_evm::{ path::TokenPair, }, }; +use gem_jsonrpc::types::JsonRpcResponse; pub fn build_quote_exact_single_request(token_in: &Address, v4_quoter: &str, amount_in: u128, pool: &PoolKey) -> EthereumRpc { let zero_for_one = *token_in == pool.currency0; @@ -51,7 +52,7 @@ pub fn decode_quoter_response(response: &JsonRpcResponse) -> Result<(U25 #[cfg(test)] mod tests { use super::*; - use crate::swapper::uniswap::v4::path::{build_pool_keys, build_quote_exact_params}; + use crate::uniswap::v4::path::{build_pool_keys, build_quote_exact_params}; use alloy_primitives::{address, hex::encode_prefixed as HexEncode}; use alloy_sol_types::SolValue; use gem_evm::uniswap::{FeeTier, path::get_base_pair}; diff --git a/gemstone/tests/integration_test.rs b/crates/swapper/tests/integration_test.rs similarity index 53% rename from gemstone/tests/integration_test.rs rename to crates/swapper/tests/integration_test.rs index fd545d479..0433da1e8 100644 --- a/gemstone/tests/integration_test.rs +++ b/crates/swapper/tests/integration_test.rs @@ -1,19 +1,20 @@ -#![cfg(feature = "reqwest_provider")] +#![cfg(all(feature = "reqwest_provider", feature = "swap_integration_tests"))] #[cfg(test)] -mod tests { +mod network_tests { use gem_solana::{jsonrpc::SolanaRpc, models::blockhash::SolanaBlockhashResult}; - use gemstone::{ - config::swap_config::{SwapReferralFee, SwapReferralFees, get_swap_config}, - network::{alien_provider::NativeProvider, jsonrpc_client_with_chain}, - swapper::{GemSwapper, across::Across, cetus::Cetus, models::*, uniswap::v4::UniswapV4, *}, - }; use primitives::{AssetId, Chain}; - use std::{collections::HashMap, sync::Arc, time::SystemTime}; + use std::{sync::Arc, time::SystemTime}; + use swapper::{ + FetchQuoteData, NativeProvider, Options, QuoteRequest, RpcClient, SwapperError, SwapperMode, SwapperProvider, + client_factory::create_client_with_chain, + config::{ReferralFee, ReferralFees, get_swap_config}, + }; + use swapper::{across::Across, cetus::Cetus, uniswap}; #[tokio::test] async fn test_solana_json_rpc() -> Result<(), String> { - let rpc_client = jsonrpc_client_with_chain(Arc::new(NativeProvider::default()), Chain::Solana); + let rpc_client = create_client_with_chain(Arc::new(NativeProvider::default()), Chain::Solana); let response: SolanaBlockhashResult = rpc_client.request(SolanaRpc::GetLatestBlockhash).await.map_err(|e| e.to_string())?; let recent_blockhash = response.value.blockhash; @@ -30,80 +31,24 @@ mod tests { Ok(()) } - #[tokio::test] - async fn test_swapper_get_quote_by_output() -> Result<(), SwapperError> { - let network_provider = Arc::new(NativeProvider::default()); - let swapper = GemSwapper::new(network_provider); - - let trade_pairs: HashMap = HashMap::from([ - ( - Chain::Abstract, - ( - AssetId::from_chain(Chain::Abstract), - AssetId::from(Chain::Abstract, Some("0x84A71ccD554Cc1b02749b35d22F684CC8ec987e1".to_string())), - ), - ), - ( - Chain::Ethereum, - ( - AssetId::from_chain(Chain::Ethereum), - AssetId::from(Chain::Ethereum, Some("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string())), - ), - ), - ]); - - let (from_asset, to_asset) = trade_pairs.get(&Chain::Abstract).cloned().unwrap(); - - let options = SwapperOptions { - slippage: 100.into(), - fee: Some(SwapReferralFees::evm(SwapReferralFee { - bps: 25, - address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), - })), - preferred_providers: vec![], - }; - - let request = SwapperQuoteRequest { - from_asset: from_asset.into(), - to_asset: to_asset.into(), - wallet_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), - destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), - value: "20000000000000000".into(), // 0.02 ETH - mode: SwapperMode::ExactIn, - options, - }; - - let quotes = swapper.fetch_quote(&request).await?; - assert_eq!(quotes.len(), 1); - - let quote = "es[0]; - println!("<== quote: {:?}", quote); - assert!(quote.to_value.parse::().unwrap() > 0); - - let quote_data = swapper.fetch_quote_data("e, FetchQuoteData::EstimateGas).await?; - println!("<== quote_data: {:?}", quote_data); - - Ok(()) - } - #[tokio::test] async fn test_across_quote() -> Result<(), SwapperError> { - let swap_provider = Across::boxed(); let network_provider = Arc::new(NativeProvider::default()); - let mut options = SwapperOptions { + let swap_provider = Across::boxed(network_provider.clone()); + let mut options = Options { slippage: 100.into(), - fee: Some(SwapReferralFees::evm(SwapReferralFee { + fee: Some(ReferralFees::evm(ReferralFee { bps: 25, address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), })), preferred_providers: vec![], }; - options.fee.as_mut().unwrap().evm_bridge = SwapReferralFee { + options.fee.as_mut().unwrap().evm_bridge = ReferralFee { bps: 25, address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), }; - let request = SwapperQuoteRequest { + let request = QuoteRequest { from_asset: AssetId::from_chain(Chain::Optimism).into(), to_asset: AssetId::from_chain(Chain::Arbitrum).into(), wallet_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), @@ -114,16 +59,14 @@ mod tests { }; let now = SystemTime::now(); - let quote = swap_provider.fetch_quote(&request, network_provider.clone()).await?; + let quote = swap_provider.fetch_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, network_provider.clone(), FetchQuoteData::EstimateGas) - .await?; + let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::EstimateGas).await?; println!("<== quote_data: {:?}", quote_data); Ok(()) @@ -131,18 +74,18 @@ mod tests { #[tokio::test] async fn test_v4_quoter() -> Result<(), SwapperError> { - let swap_provider = UniswapV4::boxed(); let network_provider = Arc::new(NativeProvider::default()); - let options = SwapperOptions { + let swap_provider = uniswap::default::boxed_uniswap_v4(network_provider.clone()); + let options = Options { slippage: 100.into(), - fee: Some(SwapReferralFees::evm(SwapReferralFee { + fee: Some(ReferralFees::evm(ReferralFee { bps: 25, address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), })), preferred_providers: vec![SwapperProvider::UniswapV4], }; - let request = SwapperQuoteRequest { + let request = QuoteRequest { from_asset: AssetId::from_chain(Chain::Unichain).into(), to_asset: AssetId::from(Chain::Unichain, Some("0x078D782b760474a361dDA0AF3839290b0EF57AD6".to_string())).into(), wallet_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), @@ -153,16 +96,14 @@ mod tests { }; let now = SystemTime::now(); - let quote = swap_provider.fetch_quote(&request, network_provider.clone()).await?; + let quote = swap_provider.fetch_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, network_provider.clone(), FetchQuoteData::EstimateGas) - .await?; + let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::EstimateGas).await?; println!("<== quote_data: {:?}", quote_data); Ok(()) @@ -170,16 +111,16 @@ mod tests { #[tokio::test] async fn test_cetus_swap() -> Result<(), Box> { - let swap_provider = Cetus::boxed(); let network_provider = Arc::new(NativeProvider::default()); + let swap_provider = Cetus::::boxed(network_provider.clone()); let config = get_swap_config(); - let options = SwapperOptions { + let options = Options { slippage: 50.into(), fee: Some(config.referral_fee), preferred_providers: vec![], }; - let request = SwapperQuoteRequest { + let request = QuoteRequest { from_asset: AssetId::from_chain(Chain::Sui).into(), to_asset: AssetId { chain: Chain::Sui, @@ -193,10 +134,10 @@ mod tests { options, }; - let quote = swap_provider.fetch_quote(&request, network_provider.clone()).await?; + let quote = swap_provider.fetch_quote(&request).await?; println!("{:?}", quote); - let quote_data = swap_provider.fetch_quote_data("e, network_provider.clone(), FetchQuoteData::None).await?; + let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::None).await?; println!("{:?}", quote_data); Ok(()) diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index fe21720c9..bc581cee9 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -14,10 +14,11 @@ name = "gemstone" [features] default = [] -reqwest_provider = ["dep:reqwest"] +reqwest_provider = ["dep:reqwest", "swapper/reqwest_provider"] swap_integration_tests = ["reqwest_provider"] [dependencies] +swapper = { path = "../crates/swapper" } primitives = { path = "../crates/primitives" } gem_cosmos = { path = "../crates/gem_cosmos", features = ["rpc"] } gem_solana = { path = "../crates/gem_solana", features = ["rpc"] } @@ -28,7 +29,6 @@ gem_sui = { path = "../crates/gem_sui", features = ["rpc"] } gem_aptos = { path = "../crates/gem_aptos", features = ["rpc"] } gem_hash = { path = "../crates/gem_hash" } gem_jsonrpc = { path = "../crates/gem_jsonrpc" } -anyhow = "1.0" gem_client = { path = "../crates/gem_client" } gem_hypercore = { path = "../crates/gem_hypercore" } gem_bitcoin = { path = "../crates/gem_bitcoin", features = ["rpc"] } @@ -40,41 +40,25 @@ gem_near = { path = "../crates/gem_near", features = ["rpc"] } gem_polkadot = { path = "../crates/gem_polkadot", features = ["rpc"] } serde_serializers = { path = "../crates/serde_serializers" } chain_traits = { path = "../crates/chain_traits" } -number_formatter = { path = "../crates/number_formatter" } reqwest = { workspace = true, optional = true } -bcs.workspace = true -sui-types = { workspace = true } -sui-transaction-builder = { workspace = true } # uniffi uniffi.workspace = true -strum = { workspace = true } chrono = { workspace = true } -base64.workspace = true serde.workspace = true serde_json.workspace = true -serde_urlencoded.workspace = true async-trait.workspace = true alloy-primitives.workspace = true -alloy-sol-types.workspace = true hex.workspace = true num-bigint.workspace = true -num-traits.workspace = true futures.workspace = true bs58 = { workspace = true } -solana-primitives = "0.2.1" -bigdecimal.workspace = true -rand.workspace = true [build-dependencies] uniffi = { workspace = true, features = ["build"] } [dev-dependencies] tokio.workspace = true - -[[test]] -name = "integration_test" -test = false diff --git a/gemstone/src/alien/client.rs b/gemstone/src/alien/client.rs new file mode 100644 index 000000000..9e435af66 --- /dev/null +++ b/gemstone/src/alien/client.rs @@ -0,0 +1,11 @@ +use super::AlienProvider; +use super::provider::AlienProviderWrapper; +use std::sync::Arc; +use swapper::{RpcClient, RpcProvider}; + +pub type AlienClient = RpcClient; + +pub fn new_alien_client(base_url: String, provider: Arc) -> AlienClient { + let wrapper: Arc = Arc::new(AlienProviderWrapper { provider }); + RpcClient::new(base_url, wrapper) +} diff --git a/gemstone/src/alien/error.rs b/gemstone/src/alien/error.rs index 77ac47cd7..8e4aedf07 100644 --- a/gemstone/src/alien/error.rs +++ b/gemstone/src/alien/error.rs @@ -1,18 +1,8 @@ -#[derive(Debug, Clone, uniffi::Error)] +pub type AlienError = swapper::AlienError; + +#[uniffi::remote(Enum)] pub enum AlienError { RequestError { msg: String }, ResponseError { msg: String }, SigningError { msg: String }, } - -impl std::fmt::Display for AlienError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::RequestError { msg } => write!(f, "Request error: {}", msg), - Self::ResponseError { msg } => write!(f, "Response error: {}", msg), - Self::SigningError { msg } => write!(f, "Signing error: {}", msg), - } - } -} - -impl std::error::Error for AlienError {} diff --git a/gemstone/src/alien/mime.rs b/gemstone/src/alien/mime.rs deleted file mode 100644 index 6a190d1b9..000000000 --- a/gemstone/src/alien/mime.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub const CONTENT_TYPE: &str = "Content-Type"; -pub const JSON: &str = "application/json"; diff --git a/gemstone/src/alien/mod.rs b/gemstone/src/alien/mod.rs index 500c184d9..1e630b668 100644 --- a/gemstone/src/alien/mod.rs +++ b/gemstone/src/alien/mod.rs @@ -1,22 +1,13 @@ +pub mod client; pub mod error; -pub mod mime; -pub mod mock; pub mod provider; #[cfg(feature = "reqwest_provider")] pub mod reqwest_provider; pub mod signer; pub mod target; +pub use client::{AlienClient, new_alien_client}; pub use error::AlienError; -pub use provider::AlienProvider; +pub use provider::{AlienProvider, AlienProviderWrapper}; pub use signer::AlienSigner; pub use target::{AlienHttpMethod, AlienTarget, X_CACHE_TTL}; - -use primitives::Chain; -use std::str::FromStr; - -uniffi::custom_type!(Chain, String, { - remote, - lower: |s| s.to_string(), - try_lift: |s| Chain::from_str(&s).map_err(|_| uniffi::deps::anyhow::Error::msg("Invalid Chain")), -}); diff --git a/gemstone/src/alien/provider.rs b/gemstone/src/alien/provider.rs index 178647882..7780a3e84 100644 --- a/gemstone/src/alien/provider.rs +++ b/gemstone/src/alien/provider.rs @@ -1,8 +1,9 @@ use super::{AlienError, AlienTarget}; use async_trait::async_trait; +use gem_jsonrpc::RpcProvider as GenericRpcProvider; use primitives::Chain; -use std::fmt::Debug; +use std::{fmt::Debug, sync::Arc}; pub type Data = Vec; @@ -13,3 +14,24 @@ pub trait AlienProvider: Send + Sync + Debug { async fn batch_request(&self, targets: Vec) -> Result, AlienError>; fn get_endpoint(&self, chain: Chain) -> Result; } + +#[derive(Debug)] +pub struct AlienProviderWrapper { + pub provider: Arc, +} + +#[async_trait] +impl GenericRpcProvider for AlienProviderWrapper { + type Error = AlienError; + + async fn request(&self, target: AlienTarget) -> Result { + self.provider.request(target).await + } + async fn batch_request(&self, targets: Vec) -> Result, Self::Error> { + self.provider.batch_request(targets).await + } + + fn get_endpoint(&self, chain: Chain) -> Result { + self.provider.get_endpoint(chain) + } +} diff --git a/gemstone/src/alien/reqwest_provider.rs b/gemstone/src/alien/reqwest_provider.rs index bf97dcfb2..2d14afbff 100644 --- a/gemstone/src/alien/reqwest_provider.rs +++ b/gemstone/src/alien/reqwest_provider.rs @@ -1,104 +1,21 @@ -use super::{AlienError, AlienHttpMethod, AlienProvider, AlienTarget, provider::Data}; -use primitives::{Chain, node_config::get_nodes_for_chain}; - +use super::{AlienError, AlienProvider, AlienTarget, provider::Data}; use async_trait::async_trait; -use futures::{TryFutureExt, future::try_join_all}; -use reqwest::Client; +use gem_jsonrpc::RpcProvider as GenericRpcProvider; +use primitives::Chain; -#[derive(Debug)] -pub struct NativeProvider { - pub client: Client, - debug: bool, -} - -impl NativeProvider { - pub fn new() -> Self { - Self { - client: Client::new(), - debug: true, - } - } - - pub fn set_debug(mut self, debug: bool) -> Self { - self.debug = debug; - self - } -} - -impl Default for NativeProvider { - fn default() -> Self { - Self::new() - } -} +pub use swapper::NativeProvider; #[async_trait] impl AlienProvider for NativeProvider { - fn get_endpoint(&self, chain: Chain) -> Result { - let nodes = get_nodes_for_chain(chain); - if nodes.is_empty() { - return Err(AlienError::ResponseError { - msg: format!("not supported chain: {chain:?}"), - }); - } - Ok(nodes[0].url.clone()) - } - async fn request(&self, target: AlienTarget) -> Result { - if self.debug { - println!("==> request: url: {:?}, method: {:?}", target.url, target.method); - } - let mut req = match target.method { - AlienHttpMethod::Get => self.client.get(target.url), - AlienHttpMethod::Post => self.client.post(target.url), - AlienHttpMethod::Put => self.client.put(target.url), - AlienHttpMethod::Delete => self.client.delete(target.url), - AlienHttpMethod::Head => self.client.head(target.url), - AlienHttpMethod::Patch => self.client.patch(target.url), - AlienHttpMethod::Options => todo!(), - }; - if let Some(headers) = target.headers { - for (key, value) in headers.iter() { - req = req.header(key, value); - } - } - if let Some(body) = target.body { - if self.debug && body.len() <= 4096 { - if let Ok(json) = serde_json::from_slice::(&body) { - println!("=== json: {json:?}"); - } else { - println!("=== body: {:?}", String::from_utf8(body.to_vec()).unwrap()); - } - } - req = req.body(body); - } - - let response = req - .send() - .map_err(|e| AlienError::ResponseError { - msg: format!("reqwest send error: {e:?}"), - }) - .await?; - let bytes = response - .bytes() - .map_err(|e| AlienError::ResponseError { - msg: format!("request error: {e:?}"), - }) - .await?; - if self.debug { - println!("<== response body size: {:?}", bytes.len()); - } - if self.debug && bytes.len() <= 4096 { - if let Ok(json) = serde_json::from_slice::(&bytes) { - println!("=== json: {json:?}"); - } else { - println!("=== body: {:?}", String::from_utf8(bytes.to_vec()).unwrap()); - } - } - Ok(bytes.to_vec()) + ::request(self, target).await } async fn batch_request(&self, targets: Vec) -> Result, AlienError> { - let futures = targets.into_iter().map(|target| self.request(target)); - try_join_all(futures).await + ::batch_request(self, targets).await + } + + fn get_endpoint(&self, chain: Chain) -> Result { + ::get_endpoint(self, chain) } } diff --git a/gemstone/src/alien/target.rs b/gemstone/src/alien/target.rs index 09932f25c..2b5c749ab 100644 --- a/gemstone/src/alien/target.rs +++ b/gemstone/src/alien/target.rs @@ -1,8 +1,10 @@ -use std::{collections::HashMap, fmt::Debug}; +pub type AlienTarget = swapper::Target; +pub type AlienHttpMethod = swapper::HttpMethod; +pub use gem_jsonrpc::X_CACHE_TTL; -pub const X_CACHE_TTL: &str = "x-cache-ttl"; +use std::collections::HashMap; -#[derive(Debug, Clone, uniffi::Record)] +#[uniffi::remote(Record)] pub struct AlienTarget { pub url: String, pub method: AlienHttpMethod, @@ -10,37 +12,7 @@ pub struct AlienTarget { pub body: Option>, } -impl AlienTarget { - pub fn get(url: &str) -> Self { - Self { - url: url.into(), - method: AlienHttpMethod::Get, - headers: None, - body: None, - } - } - - pub fn post_json(url: &str, body: serde_json::Value) -> Self { - Self { - url: url.into(), - method: AlienHttpMethod::Post, - headers: Some(HashMap::from([("Content-Type".into(), "application/json".into())])), - body: Some(serde_json::to_vec(&body).unwrap()), - } - } - - pub fn set_cache_ttl(mut self, ttl: u64) -> Self { - if self.headers.is_none() { - self.headers = Some(HashMap::new()); - } - if let Some(headers) = self.headers.as_mut() { - headers.insert(X_CACHE_TTL.into(), ttl.to_string()); - } - self - } -} - -#[derive(Debug, Clone, Copy, PartialEq, uniffi::Enum)] +#[uniffi::remote(Enum)] pub enum AlienHttpMethod { Get, Post, @@ -51,21 +23,6 @@ pub enum AlienHttpMethod { Patch, } -impl From for String { - fn from(value: AlienHttpMethod) -> Self { - match value { - AlienHttpMethod::Get => "GET", - AlienHttpMethod::Post => "POST", - AlienHttpMethod::Put => "PUT", - AlienHttpMethod::Delete => "DELETE", - AlienHttpMethod::Head => "HEAD", - AlienHttpMethod::Options => "OPTIONS", - AlienHttpMethod::Patch => "PATCH", - } - .into() - } -} - #[uniffi::export] fn alien_method_to_string(method: AlienHttpMethod) -> String { method.into() diff --git a/gemstone/src/block_explorer/mod.rs b/gemstone/src/block_explorer/mod.rs index 3ea54167f..a15ac9323 100644 --- a/gemstone/src/block_explorer/mod.rs +++ b/gemstone/src/block_explorer/mod.rs @@ -5,7 +5,7 @@ use primitives::{ }; use std::str::FromStr; -use crate::swapper::SwapperProvider; +use swapper::SwapperProvider; #[derive(uniffi::Object)] pub struct Explorer { diff --git a/gemstone/src/config/swap_config.rs b/gemstone/src/config/swap_config.rs index 1e41ba028..50c9f104b 100644 --- a/gemstone/src/config/swap_config.rs +++ b/gemstone/src/config/swap_config.rs @@ -1,12 +1,10 @@ -use crate::swapper::{SwapperSlippage, SwapperSlippageMode}; use primitives::Chain; +use swapper::{SwapperSlippage, config as swap_config}; -pub static DEFAULT_SLIPPAGE_BPS: u32 = 100; -pub static DEFAULT_SWAP_FEE_BPS: u32 = 50; -pub static DEFAULT_CHAINFLIP_FEE_BPS: u32 = 45; -pub static DEFAULT_STABLE_SWAP_REFERRAL_BPS: u32 = 25; +pub use swap_config::get_swap_config; +pub use swap_config::{Config as SwapConfig, ReferralFee as SwapReferralFee, ReferralFees as SwapReferralFees}; -#[derive(uniffi::Record, Debug, Clone, PartialEq)] +#[uniffi::remote(Record)] pub struct SwapConfig { pub default_slippage: SwapperSlippage, pub permit2_expiration: u64, @@ -15,7 +13,7 @@ pub struct SwapConfig { pub high_price_impact_percent: u32, } -#[derive(uniffi::Record, Default, Debug, Clone, PartialEq)] +#[uniffi::remote(Record)] pub struct SwapReferralFees { pub evm: SwapReferralFee, pub evm_bridge: SwapReferralFee, @@ -26,103 +24,13 @@ pub struct SwapReferralFees { pub tron: SwapReferralFee, } -#[derive(uniffi::Record, Default, Debug, Clone, PartialEq)] +#[uniffi::remote(Record)] pub struct SwapReferralFee { pub address: String, pub bps: u32, } -impl SwapReferralFees { - pub fn evm(evm: SwapReferralFee) -> SwapReferralFees { - SwapReferralFees { - evm, - evm_bridge: SwapReferralFee::default(), - solana: SwapReferralFee::default(), - thorchain: SwapReferralFee::default(), - sui: SwapReferralFee::default(), - ton: SwapReferralFee::default(), - tron: SwapReferralFee::default(), - } - } - - pub fn update_all_bps(&mut self, bps: u32) { - self.iter_mut().for_each(|fee| fee.update_bps(bps)); - } - - fn iter_mut(&mut self) -> impl Iterator { - [ - &mut self.evm, - &mut self.evm_bridge, - &mut self.solana, - &mut self.thorchain, - &mut self.sui, - &mut self.ton, - &mut self.tron, - ] - .into_iter() - } -} - -impl SwapReferralFee { - pub fn update_bps(&mut self, bps: u32) { - if !self.address.is_empty() || self.bps > 0 { - self.bps = bps; - } - } -} - -pub fn get_swap_config() -> SwapConfig { - SwapConfig { - default_slippage: SwapperSlippage { - bps: DEFAULT_SLIPPAGE_BPS, - mode: SwapperSlippageMode::Exact, - }, - permit2_expiration: 60 * 60 * 24 * 30, // 30 days - permit2_sig_deadline: 60 * 30, // 30 minutes - referral_fee: SwapReferralFees { - evm: SwapReferralFee { - address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), - bps: DEFAULT_SWAP_FEE_BPS, - }, - evm_bridge: SwapReferralFee { - address: "0x0D9DAB1A248f63B0a48965bA8435e4de7497a3dC".into(), - bps: DEFAULT_STABLE_SWAP_REFERRAL_BPS, - }, - solana: SwapReferralFee { - address: "5fmLrs2GuhfDP1B51ziV5Kd1xtAr9rw1jf3aQ4ihZ2gy".into(), - bps: DEFAULT_SWAP_FEE_BPS, - }, - thorchain: SwapReferralFee { - address: "g1".into(), - bps: DEFAULT_SWAP_FEE_BPS, - }, - sui: SwapReferralFee { - address: "0x9d6b98b18fd26b5efeec68d020dcf1be7a94c2c315353779bc6b3aed44188ddf".into(), - bps: DEFAULT_SWAP_FEE_BPS, - }, - ton: SwapReferralFee { - address: "UQDxJKarPSp0bCta9DFgp81Mpt5hpGbuVcSxwfeza0Bin201".into(), - bps: DEFAULT_SWAP_FEE_BPS, - }, - tron: SwapReferralFee { - address: "TYeyZXywpA921LEtw2PF3obK4B8Jjgpp32".into(), - bps: DEFAULT_SWAP_FEE_BPS, - }, - }, - high_price_impact_percent: 10, - } -} - #[uniffi::export] pub fn get_default_slippage(chain: &Chain) -> SwapperSlippage { - match chain { - Chain::Solana => SwapperSlippage { - bps: DEFAULT_SLIPPAGE_BPS * 3, - mode: SwapperSlippageMode::Auto, - }, - _ => SwapperSlippage { - bps: DEFAULT_SLIPPAGE_BPS, - mode: SwapperSlippageMode::Exact, - }, - } + swap_config::get_default_slippage(chain) } diff --git a/gemstone/src/ethereum/jsonrpc.rs b/gemstone/src/ethereum/jsonrpc.rs deleted file mode 100644 index cded9ed10..000000000 --- a/gemstone/src/ethereum/jsonrpc.rs +++ /dev/null @@ -1,79 +0,0 @@ -use alloy_primitives::{U256, hex::decode as HexDecode}; -use alloy_sol_types::SolCall; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -use crate::{ - debug_println, - network::{AlienProvider, jsonrpc_client_with_chain}, - swapper::SwapperError, -}; - -use gem_evm::{ - jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, - multicall3::{self, IMulticall3}, - parse_u256, -}; -use primitives::{Chain, EVMChain}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TxReceiptLog { - pub address: String, - pub topics: Vec, - pub data: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TxReceipt { - pub status: String, - pub logs: Vec, -} - -pub async fn fetch_gas_price(provider: Arc, chain: Chain) -> Result { - let call = EthereumRpc::GasPrice; - let client = jsonrpc_client_with_chain(provider, chain); - let value: String = client.request(call).await?; - - parse_u256(&value).ok_or(SwapperError::InvalidAmount("invalid gas price".into())) -} - -pub async fn estimate_gas(provider: Arc, chain: Chain, tx: TransactionObject) -> Result { - let call = EthereumRpc::EstimateGas(tx, BlockParameter::Latest); - let client = jsonrpc_client_with_chain(provider, chain); - let value: String = client.request(call).await?; - parse_u256(&value).ok_or(SwapperError::InvalidAmount("invalid gas limit".into())) -} - -pub async fn fetch_tx_receipt(provider: Arc, chain: Chain, tx_hash: &str) -> Result { - let call = EthereumRpc::GetTransactionReceipt(tx_hash.into()); - let client = jsonrpc_client_with_chain(provider, chain); - let result: TxReceipt = client.request(call).await?; - Ok(result) -} - -pub async fn multicall3_call( - provider: Arc, - chain: &Chain, - calls: Vec, -) -> Result, SwapperError> { - for (_idx, _call) in calls.iter().enumerate() { - debug_println!( - "call {_idx}: target {:?}, calldata: {:?}, allowFailure: {:?}", - _call.target, - hex::encode(&_call.callData), - _call.allowFailure - ); - } - let evm_chain = EVMChain::from_chain(*chain).ok_or(SwapperError::NotSupportedChain)?; - let multicall_address = multicall3::deployment_by_chain(&evm_chain); - let data = IMulticall3::aggregate3Call { calls }.abi_encode(); - let call = EthereumRpc::Call(TransactionObject::new_call(multicall_address, data), BlockParameter::Latest); - - let client = jsonrpc_client_with_chain(provider.clone(), *chain); - let result: String = client.request(call).await?; - let hex_data = HexDecode(result).map_err(|e| SwapperError::NetworkError(e.to_string()))?; - - let decoded = IMulticall3::aggregate3Call::abi_decode_returns(&hex_data).map_err(|_| SwapperError::ABIError("failed to decode aggregate3Call".into()))?; - - Ok(decoded) -} diff --git a/gemstone/src/ethereum/mod.rs b/gemstone/src/ethereum/mod.rs index fcccdccdb..4a9df29e2 100644 --- a/gemstone/src/ethereum/mod.rs +++ b/gemstone/src/ethereum/mod.rs @@ -1,4 +1,3 @@ pub mod decoder; -pub mod jsonrpc; pub use decoder::{EthereumDecoder, GemDecodedCall, GemDecodedCallParam}; diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index 7fae6500c..7765c4852 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -1,5 +1,6 @@ +use crate::alien::{AlienProvider, new_alien_client}; use crate::models::*; -use crate::network::{AlienClient, AlienProvider, jsonrpc_client_with_chain}; +use crate::network::jsonrpc_client_with_chain; use chain_traits::ChainTraits; use gem_algorand::rpc::AlgorandClientIndexer; use gem_algorand::rpc::client::AlgorandClient; @@ -77,7 +78,7 @@ impl GemGateway { } pub async fn provider_with_url(&self, chain: Chain, url: String) -> Result, GatewayError> { - let alien_client = AlienClient::new(url.clone(), self.provider.clone()); + let alien_client = new_alien_client(url.clone(), self.provider.clone()); match chain { Chain::HyperCore => { let preferences = Arc::new(PreferencesWrapper { @@ -226,13 +227,11 @@ impl GemGateway { } pub async fn get_transaction_status(&self, chain: Chain, request: GemTransactionStateRequest) -> Result { - let status = self - .provider(chain) + self.provider(chain) .await? .get_transaction_status(request.into()) .await - .map_err(|e| GatewayError::NetworkError(e.to_string()))?; - Ok(status) + .map_err(|e| GatewayError::NetworkError(e.to_string())) } pub async fn get_chain_id(&self, chain: Chain) -> Result { diff --git a/gemstone/src/gem_swapper/error.rs b/gemstone/src/gem_swapper/error.rs new file mode 100644 index 000000000..43a03e773 --- /dev/null +++ b/gemstone/src/gem_swapper/error.rs @@ -0,0 +1,19 @@ +pub type SwapperError = swapper::SwapperError; + +#[uniffi::remote(Enum)] +pub enum SwapperError { + NotSupportedChain, + NotSupportedAsset, + NotSupportedPair, + NoAvailableProvider, + InvalidAddress(String), + InvalidAmount(String), + InputAmountTooSmall, + InvalidRoute, + NetworkError(String), + ABIError(String), + ComputeQuoteError(String), + TransactionError(String), + NoQuoteAvailable, + NotImplemented, +} diff --git a/gemstone/src/gem_swapper/mod.rs b/gemstone/src/gem_swapper/mod.rs new file mode 100644 index 000000000..267b4bb5a --- /dev/null +++ b/gemstone/src/gem_swapper/mod.rs @@ -0,0 +1,59 @@ +mod error; +mod permit2; +use error::SwapperError; +use permit2::*; +mod remote_types; +use remote_types::*; +type Swapper = swapper::swapper::GemSwapper; + +use crate::alien::{AlienProvider, AlienProviderWrapper}; +use primitives::{AssetId, Chain}; +use std::sync::Arc; + +#[derive(Debug, uniffi::Object)] +pub struct GemSwapper { + inner: Swapper, +} + +#[uniffi::export] +impl GemSwapper { + #[uniffi::constructor] + pub fn new(rpc_provider: Arc) -> Self { + let wrapper = AlienProviderWrapper { provider: rpc_provider }; + Self { + inner: Swapper::new(Arc::new(wrapper)), + } + } + + pub fn supported_chains(&self) -> Vec { + self.inner.supported_chains() + } + + pub fn supported_chains_for_from_asset(&self, asset_id: &AssetId) -> SwapperAssetList { + self.inner.supported_chains_for_from_asset(asset_id) + } + + pub fn get_providers(&self) -> Vec { + self.inner.get_providers() + } + + pub async fn fetch_quote(&self, request: &SwapperQuoteRequest) -> Result, SwapperError> { + self.inner.fetch_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 fetch_permit2_for_quote(&self, quote: &SwapperQuote) -> Result, SwapperError> { + self.inner.fetch_permit2_for_quote(quote).await + } + + pub async fn fetch_quote_data(&self, quote: &SwapperQuote, data: FetchQuoteData) -> Result { + self.inner.fetch_quote_data(quote, data).await + } + + pub async fn get_swap_result(&self, chain: Chain, swap_provider: SwapperProvider, transaction_hash: &str) -> Result { + self.inner.get_swap_result(chain, swap_provider, transaction_hash).await + } +} diff --git a/gemstone/src/gem_swapper/permit2.rs b/gemstone/src/gem_swapper/permit2.rs new file mode 100644 index 000000000..72dd9f56a --- /dev/null +++ b/gemstone/src/gem_swapper/permit2.rs @@ -0,0 +1,43 @@ +use primitives::Chain; +use swapper::SwapperError; + +type Permit2Data = swapper::permit2_data::Permit2Data; +type PermitSingle = swapper::permit2_data::PermitSingle; +type Permit2Detail = swapper::permit2_data::Permit2Detail; + +pub type Permit2ApprovalData = swapper::models::Permit2ApprovalData; + +#[uniffi::remote(Record)] +pub struct Permit2Detail { + pub token: String, + pub amount: String, + pub expiration: u64, + pub nonce: u64, +} + +#[uniffi::remote(Record)] +pub struct PermitSingle { + pub details: Permit2Detail, + pub spender: String, + pub sig_deadline: u64, +} + +#[uniffi::remote(Record)] +pub struct Permit2Data { + pub permit_single: PermitSingle, + pub signature: Vec, +} + +#[uniffi::remote(Record)] +pub struct Permit2ApprovalData { + pub token: String, + pub spender: String, + pub value: String, + pub permit2_contract: String, + pub permit2_nonce: u64, +} + +#[uniffi::export] +pub fn permit2_data_to_eip712_json(chain: Chain, data: PermitSingle, contract: &str) -> Result { + swapper::permit2_data::permit2_data_to_eip712_json(chain, data, contract) +} diff --git a/gemstone/src/gem_swapper/remote_types.rs b/gemstone/src/gem_swapper/remote_types.rs new file mode 100644 index 000000000..16c0b1064 --- /dev/null +++ b/gemstone/src/gem_swapper/remote_types.rs @@ -0,0 +1,172 @@ +use crate::config::swap_config::SwapReferralFees; +use primitives::{AssetId, Chain, swap::ApprovalData as GemApprovalData}; +use std::str::FromStr; +pub use swapper::{ + AssetList as SwapperAssetList, FetchQuoteData, Options as SwapperOptions, ProviderData as SwapperProviderData, ProviderType as SwapperProviderType, + Quote as SwapperQuote, QuoteRequest as SwapperQuoteRequest, Route as SwapperRoute, SwapResult as SwapperSwapResult, SwapperMode, SwapperProvider, + SwapperProviderMode, SwapperQuoteAsset, SwapperQuoteData, SwapperSlippage, SwapperSlippageMode, SwapperSwapStatus, permit2_data::Permit2Data, +}; + +#[derive(Debug, Clone, PartialEq, uniffi::Object)] +pub struct SwapProviderConfig(SwapperProviderType); + +#[uniffi::export] +impl SwapProviderConfig { + #[uniffi::constructor] + pub fn new(id: SwapperProvider) -> Self { + Self(SwapperProviderType::new(id)) + } + #[uniffi::constructor] + pub fn from_string(id: String) -> Self { + let id = SwapperProvider::from_str(&id).unwrap(); + Self(SwapperProviderType::new(id)) + } + pub fn inner(&self) -> SwapperProviderType { + self.0.clone() + } +} + +#[uniffi::remote(Enum)] +pub enum FetchQuoteData { + Permit2(Permit2Data), + EstimateGas, + None, +} + +#[uniffi::remote(Record)] +pub struct SwapperSwapResult { + pub status: SwapperSwapStatus, + pub from_chain: Chain, + pub from_tx_hash: String, + pub to_chain: Option, + pub to_tx_hash: Option, +} + +#[uniffi::remote(Record)] +pub struct SwapperAssetList { + pub chains: Vec, + pub asset_ids: Vec, +} + +#[uniffi::remote(Record)] +pub struct SwapperProviderType { + pub id: SwapperProvider, + pub name: String, + pub protocol: String, + pub protocol_id: String, +} + +#[uniffi::remote(Record)] +pub struct SwapperOptions { + pub slippage: SwapperSlippage, + pub fee: Option, + pub preferred_providers: Vec, +} + +#[uniffi::remote(Record)] +pub struct SwapperQuoteRequest { + pub from_asset: SwapperQuoteAsset, + pub to_asset: SwapperQuoteAsset, + pub wallet_address: String, + pub destination_address: String, + pub value: String, + pub mode: SwapperMode, + pub options: SwapperOptions, +} + +#[uniffi::remote(Record)] +pub struct SwapperRoute { + pub input: AssetId, + pub output: AssetId, + pub route_data: String, + pub gas_limit: Option, +} + +#[uniffi::remote(Record)] +pub struct SwapperProviderData { + pub provider: SwapperProviderType, + pub slippage_bps: u32, + pub routes: Vec, +} + +#[uniffi::remote(Record)] +pub struct SwapperQuote { + pub from_value: String, + pub to_value: String, + pub data: SwapperProviderData, + pub request: SwapperQuoteRequest, + pub eta_in_seconds: Option, +} + +#[uniffi::remote(Record)] +pub struct SwapperQuoteData { + pub to: String, + pub value: String, + pub data: String, + pub approval: Option, + pub gas_limit: Option, +} + +#[uniffi::remote(Enum)] +pub enum SwapperProvider { + UniswapV3, + UniswapV4, + PancakeswapV3, + Aerodrome, + PancakeswapAptosV2, + Thorchain, + Jupiter, + Across, + Oku, + Wagmi, + Cetus, + StonfiV2, + Mayan, + Reservoir, + Symbiosis, + Chainflip, + CetusAggregator, + Relay, + Hyperliquid, +} + +#[uniffi::remote(Enum)] +pub enum SwapperProviderMode { + OnChain, + CrossChain, + Bridge, + OmniChain(Vec), +} + +#[uniffi::remote(Enum)] +pub enum SwapperMode { + ExactIn, + ExactOut, +} + +#[uniffi::remote(Record)] +pub struct SwapperSlippage { + pub bps: u32, + pub mode: SwapperSlippageMode, +} + +#[uniffi::remote(Enum)] +pub enum SwapperSlippageMode { + Auto, + Exact, +} + +#[uniffi::remote(Record)] +pub struct SwapperQuoteAsset { + pub id: String, + pub symbol: String, + pub decimals: u32, +} + +#[uniffi::remote(Enum)] +pub enum SwapperSwapStatus { + Pending, + Completed, + Failed, + Refunded, +} diff --git a/gemstone/src/lib.rs b/gemstone/src/lib.rs index 620a9afd8..d63a0a3e7 100644 --- a/gemstone/src/lib.rs +++ b/gemstone/src/lib.rs @@ -3,14 +3,13 @@ pub mod block_explorer; pub mod config; pub mod ethereum; pub mod gateway; +pub mod gem_swapper; pub mod hyperliquid; pub mod message; pub mod models; pub mod network; pub mod payment; pub mod sui; -pub mod swapper; -pub mod tron; pub mod wallet_connect; use alien::AlienError; @@ -18,11 +17,6 @@ use alien::AlienError; uniffi::setup_scaffolding!("gemstone"); static LIB_VERSION: &str = env!("CARGO_PKG_VERSION"); -#[macro_export] -macro_rules! debug_println { - ($($arg:tt)*) => (#[cfg(debug_assertions)] println!($($arg)*)); -} - #[uniffi::export] pub fn lib_version() -> String { String::from(LIB_VERSION) diff --git a/gemstone/src/models/custom_types.rs b/gemstone/src/models/custom_types.rs index 4aa640c54..f7443cac1 100644 --- a/gemstone/src/models/custom_types.rs +++ b/gemstone/src/models/custom_types.rs @@ -1,7 +1,19 @@ -use std::str::FromStr; - use chrono::{DateTime, Utc}; use num_bigint::{BigInt, BigUint}; +use primitives::{AssetId, Chain}; +use std::str::FromStr; + +uniffi::custom_type!(Chain, String, { + remote, + lower: |s| s.to_string(), + try_lift: |s| Chain::from_str(&s).map_err(|_| uniffi::deps::anyhow::Error::msg("Invalid Chain")), +}); + +uniffi::custom_type!(AssetId, String, { + remote, + lower: |s| s.to_string(), + try_lift: |s| AssetId::new(&s).ok_or_else(|| uniffi::deps::anyhow::Error::msg("Invalid AssetId")), +}); pub type GemBigInt = BigInt; pub type GemBigUint = BigUint; diff --git a/gemstone/src/network/alien_provider/mod.rs b/gemstone/src/network/alien_provider/mod.rs deleted file mode 100644 index 785799c00..000000000 --- a/gemstone/src/network/alien_provider/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub use crate::alien::{AlienError, AlienHttpMethod, AlienProvider, AlienSigner, AlienTarget, X_CACHE_TTL, error, mime, mock, provider, signer, target}; - -#[cfg(feature = "reqwest_provider")] -pub use crate::alien::reqwest_provider; -#[cfg(feature = "reqwest_provider")] -pub use crate::alien::reqwest_provider::NativeProvider; diff --git a/gemstone/src/network/mod.rs b/gemstone/src/network/mod.rs index ad9823ce1..fad558ec7 100644 --- a/gemstone/src/network/mod.rs +++ b/gemstone/src/network/mod.rs @@ -1,17 +1,14 @@ -pub mod alien_client; -pub mod alien_provider; - +use crate::alien::{AlienClient, AlienProvider, new_alien_client}; use primitives::Chain; use std::sync::Arc; -pub use alien_client::AlienClient; -pub use alien_provider::{AlienError, AlienHttpMethod, AlienProvider, AlienSigner, AlienTarget, X_CACHE_TTL}; -pub use alien_provider::{mime, mock, target}; -pub use gem_jsonrpc::client::JsonRpcClient; -pub use gem_jsonrpc::types::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, JsonRpcResult, JsonRpcResults}; +pub use gem_jsonrpc::{ + JsonRpcClient, + types::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, JsonRpcResult, JsonRpcResults}, +}; pub fn jsonrpc_client_with_chain(provider: Arc, chain: Chain) -> JsonRpcClient { let endpoint = provider.get_endpoint(chain).expect("Failed to get endpoint for chain"); - let alien_client = AlienClient::new(endpoint, provider); + let alien_client = new_alien_client(endpoint, provider); JsonRpcClient::new(alien_client) } diff --git a/gemstone/src/payment/mod.rs b/gemstone/src/payment/mod.rs index eb77d085d..feb4124d3 100644 --- a/gemstone/src/payment/mod.rs +++ b/gemstone/src/payment/mod.rs @@ -1,7 +1,5 @@ use crate::GemstoneError; -use primitives::{DecodedLinkType, PaymentURLDecoder}; - -pub mod solana_pay; +use primitives::PaymentURLDecoder; #[derive(Debug, Clone, PartialEq, uniffi::Record)] pub struct PaymentWrapper { @@ -12,19 +10,6 @@ pub struct PaymentWrapper { pub payment_link: Option, } -#[derive(Debug, Clone, PartialEq, uniffi::Enum)] -pub enum PaymentLinkType { - SolanaPay(String), -} - -impl From for PaymentLinkType { - fn from(value: DecodedLinkType) -> Self { - match value { - DecodedLinkType::SolanaPay(link) => PaymentLinkType::SolanaPay(link), - } - } -} - /// Exports functions #[uniffi::export] pub fn payment_decode_url(string: &str) -> Result { diff --git a/gemstone/src/payment/solana_pay.rs b/gemstone/src/payment/solana_pay.rs deleted file mode 100644 index 4f3a2431b..000000000 --- a/gemstone/src/payment/solana_pay.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::{ - GemstoneError, - network::{AlienProvider, AlienTarget}, -}; -use serde::Deserialize; -use std::sync::Arc; - -#[derive(Debug, uniffi::Object)] -pub struct SolanaPay { - pub provider: Arc, -} - -#[derive(Debug, Deserialize, uniffi::Record)] -pub struct SolanaPayLabel { - pub label: String, - #[serde(rename = "icon")] - pub icon_url: String, - pub title: Option, -} - -#[derive(Debug, Deserialize, uniffi::Record)] -pub struct SolanaPayTransaction { - pub message: Option, - pub transaction: Option, // base64 - pub error: Option, -} - -#[uniffi::export] -impl SolanaPay { - #[uniffi::constructor] - fn new(provider: Arc) -> Self { - Self { provider } - } - - async fn get_label(&self, link: &str) -> Result { - let target = AlienTarget::get(link); - let response = self.provider.request(target).await?; - let label = serde_json::from_slice::(&response).map_err(|_| GemstoneError::AnyError { - msg: "Failed to get solana pay label and icon".into(), - })?; - Ok(label) - } - - async fn post_account(&self, link: &str, account: &str) -> Result { - let body = serde_json::json!({ - "account": account, - }); - let target = AlienTarget::post_json(link, body); - let response = self.provider.request(target).await?; - let transaction = serde_json::from_slice::(&response).map_err(|_| GemstoneError::AnyError { - msg: "Failed to get solana pay transaction".into(), - })?; - if let Some(error) = transaction.error { - return Err(GemstoneError::AnyError { msg: error }); - } - Ok(transaction) - } -} diff --git a/gemstone/src/sui/mod.rs b/gemstone/src/sui/mod.rs index a438da6eb..f7e712d7b 100644 --- a/gemstone/src/sui/mod.rs +++ b/gemstone/src/sui/mod.rs @@ -1,6 +1,4 @@ -pub mod gas_budget; mod model; -pub mod rpc; use crate::GemstoneError; use gem_sui::models::{StakeInput, TokenTransferInput, TransferInput, UnstakeInput}; diff --git a/gemstone/src/sui/rpc/client.rs b/gemstone/src/sui/rpc/client.rs deleted file mode 100644 index b7a229d95..000000000 --- a/gemstone/src/sui/rpc/client.rs +++ /dev/null @@ -1,66 +0,0 @@ -use base64::{Engine as _, engine::general_purpose}; -use gem_sui::{ - SUI_COIN_TYPE, SUI_COIN_TYPE_FULL, - jsonrpc::{SuiData, SuiRpc}, -}; -use serde::de::DeserializeOwned; -use std::sync::Arc; -use sui_types::Address; - -use super::models::{CoinAsset, InspectResult}; -use crate::network::{AlienClient, AlienError, AlienProvider}; -use gem_jsonrpc::client::JsonRpcClient; -use primitives::Chain; - -pub struct SuiClient { - client: JsonRpcClient, -} - -impl SuiClient { - pub fn new(provider: Arc) -> Self { - let endpoint = provider.get_endpoint(Chain::Sui).unwrap(); - let alien_client = AlienClient::new(endpoint, provider); - Self { - client: JsonRpcClient::new(alien_client), - } - } - - pub async fn rpc_call(&self, rpc: SuiRpc) -> Result { - let result: T = self.client.request(rpc).await.map_err(|e| AlienError::ResponseError { msg: e.to_string() })?; - Ok(result) - } - - pub async fn get_coin_assets(&self, owner: Address) -> Result, AlienError> { - let coins: SuiData> = self.rpc_call(SuiRpc::GetAllCoins { owner: owner.to_string() }).await?; - let coins = coins - .data - .into_iter() - .map(|mut coin| { - if coin.coin_type == SUI_COIN_TYPE { - coin.coin_type = SUI_COIN_TYPE_FULL.into(); - } - coin - }) - .collect(); - Ok(coins) - } - - pub async fn get_gas_price(&self) -> Result { - let gas_price: String = self.rpc_call(SuiRpc::GetGasPrice).await?; - gas_price.parse::().map_err(|e| AlienError::ResponseError { - msg: format!("Failed to parse gas price: {e:?}"), - }) - } - - pub async fn inspect_tx_block(&self, sender: &str, tx_data: &[u8]) -> Result { - let tx_bytes_base64 = general_purpose::STANDARD.encode(tx_data); - let result: InspectResult = self.rpc_call(SuiRpc::InspectTransactionBlock(sender.to_string(), tx_bytes_base64)).await?; - - if result.error.is_some() { - return Err(AlienError::ResponseError { - msg: format!("Failed to inspect transaction: {:?}", result.error), - }); - } - Ok(result) - } -} diff --git a/gemstone/src/sui/rpc/mod.rs b/gemstone/src/sui/rpc/mod.rs deleted file mode 100644 index e10b4192a..000000000 --- a/gemstone/src/sui/rpc/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod client; -pub mod models; - -pub use client::SuiClient; -pub use models::CoinAsset; diff --git a/gemstone/src/swapper/approval/mod.rs b/gemstone/src/swapper/approval/mod.rs deleted file mode 100644 index f5a010f36..000000000 --- a/gemstone/src/swapper/approval/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod evm; -pub mod tron; - -pub use evm::{check_approval_erc20, check_approval_permit2}; diff --git a/gemstone/src/swapper/approval/tron.rs b/gemstone/src/swapper/approval/tron.rs deleted file mode 100644 index 3722fc0ec..000000000 --- a/gemstone/src/swapper/approval/tron.rs +++ /dev/null @@ -1,28 +0,0 @@ -use alloy_primitives::U256; -use std::sync::Arc; - -use crate::{ - models::GemApprovalData, - network::AlienProvider, - swapper::{SwapperError, models::ApprovalType}, - tron::client::TronClient, -}; - -pub async fn check_approval_tron( - owner_address: &str, - token_address: &str, - spender_address: &str, - amount: U256, - provider: Arc, -) -> Result { - let client = TronClient::new(provider.clone()); - let allowance = client.get_token_allowance(owner_address, token_address, spender_address).await?; - if allowance < amount { - return Ok(ApprovalType::Approve(GemApprovalData { - token: token_address.to_string(), - spender: spender_address.to_string(), - value: amount.to_string(), - })); - } - Ok(ApprovalType::None) -} diff --git a/gemstone/src/swapper/cetus/api/client.rs b/gemstone/src/swapper/cetus/api/client.rs deleted file mode 100644 index ab2bd98f9..000000000 --- a/gemstone/src/swapper/cetus/api/client.rs +++ /dev/null @@ -1,42 +0,0 @@ -use super::models::{CetusPool, Request, Response}; -use crate::{ - network::{AlienProvider, AlienTarget}, - swapper::SwapperError, -}; -use std::sync::Arc; - -const CETUS_API_URL: &str = "https://api-sui.cetus.zone/v2"; -const POOL_CACHE_TTL: u64 = 60 * 5; // 5 minutes - -pub struct CetusClient { - pub provider: Arc, -} - -impl CetusClient { - pub fn new(provider: Arc) -> Self { - Self { provider } - } - - pub async fn get_pool_by_token(&self, token_a: &str, token_b: &str) -> Result, SwapperError> { - let request = Request { - display_all_pools: true, - has_mining: true, - no_incentives: true, - coin_type: format!("{token_a},{token_b}"), - }; - let api = format!("{CETUS_API_URL}/sui/stats_pools"); - let query = serde_urlencoded::to_string(&request).unwrap(); - let url = format!("{api}?{query}"); - let mut target = AlienTarget::get(&url); - target = target.set_cache_ttl(POOL_CACHE_TTL); - - let response = self.provider.request(target).await?; - let response: Response = serde_json::from_slice(&response).map_err(|e| SwapperError::NetworkError(format!("Failed to parse json response: {e}")))?; - - if response.code != 200 { - return Err(SwapperError::NetworkError(format!("API error: {}", response.msg))); - } - - Ok(response.data.lp_list) - } -} diff --git a/gemstone/src/swapper/chainflip/broker/mod.rs b/gemstone/src/swapper/chainflip/broker/mod.rs deleted file mode 100644 index 3c1135bc8..000000000 --- a/gemstone/src/swapper/chainflip/broker/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod client; -pub mod model; - -pub use client::BrokerClient; -pub use model::*; diff --git a/gemstone/src/swapper/chainflip/client/swap.rs b/gemstone/src/swapper/chainflip/client/swap.rs deleted file mode 100644 index b8e1b79f1..000000000 --- a/gemstone/src/swapper/chainflip/client/swap.rs +++ /dev/null @@ -1,47 +0,0 @@ -use super::{ - SwapTxResponse, - model::{QuoteRequest, QuoteResponse}, -}; -use crate::{ - network::{AlienProvider, AlienTarget}, - swapper::SwapperError, -}; -use std::sync::Arc; - -const CHAINFLIP_API_URL: &str = "https://chainflip-swap.chainflip.io"; - -#[derive(Debug)] -pub struct ChainflipClient { - provider: Arc, -} - -impl ChainflipClient { - pub fn new(provider: Arc) -> Self { - Self { provider } - } - - pub async fn get_quote(&self, request: &QuoteRequest) -> Result, SwapperError> { - let query = serde_urlencoded::to_string(request).map_err(SwapperError::from)?; - let url = format!("{CHAINFLIP_API_URL}/v2/quote?{query}"); - let target = AlienTarget::get(&url); - let response = self.provider.request(target).await.map_err(SwapperError::from)?; - let value: serde_json::Value = serde_json::from_slice(&response).map_err(SwapperError::from)?; - // Check error message - if value.is_object() - && let Some(message) = value["message"].as_str() - { - return Err(SwapperError::ComputeQuoteError(message.to_string())); - } - let quotes = serde_json::from_value(value).map_err(SwapperError::from)?; - Ok(quotes) - } - - pub async fn get_tx_status(&self, tx_hash: &str) -> Result { - let url = format!("{CHAINFLIP_API_URL}/v2/swap/{tx_hash}"); - let target = AlienTarget::get(&url); - let response = self.provider.request(target).await.map_err(SwapperError::from)?; - let status = serde_json::from_slice(&response).map_err(SwapperError::from)?; - - Ok(status) - } -} diff --git a/gemstone/src/swapper/chainlink.rs b/gemstone/src/swapper/chainlink.rs deleted file mode 100644 index c8001954f..000000000 --- a/gemstone/src/swapper/chainlink.rs +++ /dev/null @@ -1,53 +0,0 @@ -use alloy_primitives::hex::decode as HexDecode; -use alloy_sol_types::SolCall; -use num_bigint::BigInt; -use num_traits::FromBytes; -use primitives::Chain; -use std::sync::Arc; - -use crate::{ - network::{AlienProvider, JsonRpcClient, jsonrpc_client_with_chain}, - swapper::SwapperError, -}; -use gem_evm::{ - chainlink::contract::{AggregatorInterface, CHAINLINK_ETH_USD_FEED}, - jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, - multicall3::{IMulticall3, create_call3, decode_call3_return}, -}; - -pub struct ChainlinkPriceFeed { - pub contract: String, - pub client: JsonRpcClient, -} - -impl ChainlinkPriceFeed { - pub fn new_eth_usd_feed(provider: Arc) -> ChainlinkPriceFeed { - ChainlinkPriceFeed { - contract: CHAINLINK_ETH_USD_FEED.into(), - client: jsonrpc_client_with_chain(provider, Chain::Ethereum), - } - } - - pub fn latest_round_call3(&self) -> IMulticall3::Call3 { - create_call3(&self.contract, AggregatorInterface::latestRoundDataCall {}) - } - - // Price is in 8 decimals - pub fn decoded_answer(result: &IMulticall3::Result) -> Result { - let decoded = - decode_call3_return::(result).map_err(|_| SwapperError::ABIError("failed to decode answer".into()))?; - let price = BigInt::from_le_bytes(&decoded.answer.to_le_bytes::<32>()); - Ok(price) - } - - #[allow(unused)] - pub async fn fetch_latest_round(&self) -> Result { - let data = AggregatorInterface::latestRoundDataCall {}.abi_encode(); - let call = EthereumRpc::Call(TransactionObject::new_call(&self.contract, data), BlockParameter::Latest); - let result: String = self.client.request(call).await?; - let hex_data = HexDecode(result).map_err(|_| SwapperError::NetworkError("failed to latest round data".into()))?; - let decoded = AggregatorInterface::latestRoundDataCall::abi_decode_returns(&hex_data).map_err(SwapperError::from)?; - - Ok(BigInt::from_le_bytes(&decoded.answer.to_le_bytes::<32>())) - } -} diff --git a/gemstone/src/swapper/custom_types.rs b/gemstone/src/swapper/custom_types.rs deleted file mode 100644 index ec9e88b54..000000000 --- a/gemstone/src/swapper/custom_types.rs +++ /dev/null @@ -1,7 +0,0 @@ -use primitives::AssetId; - -uniffi::custom_type!(AssetId, String, { - remote, - lower: |s| s.to_string(), - try_lift: |s| AssetId::new(&s).ok_or_else(|| uniffi::deps::anyhow::Error::msg("Invalid AssetId")), -}); diff --git a/gemstone/src/swapper/jupiter/client.rs b/gemstone/src/swapper/jupiter/client.rs deleted file mode 100644 index 1da50d83f..000000000 --- a/gemstone/src/swapper/jupiter/client.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::model::*; -use crate::network::{AlienError, AlienHttpMethod, AlienProvider, AlienTarget}; -use serde_json; -use std::{collections::HashMap, sync::Arc}; - -pub struct JupiterClient { - api_url: String, - provider: Arc, -} - -impl JupiterClient { - pub fn new(url: String, provider: Arc) -> Self { - Self { api_url: url, provider } - } - - pub async fn get_swap_quote(&self, request: QuoteRequest) -> Result { - let query_string = serde_urlencoded::to_string(&request).map_err(|e| AlienError::RequestError { msg: e.to_string() })?; - let target = AlienTarget::get(&format!("{}/swap/v1/quote?{}", self.api_url, &query_string)); - let response = self.provider.request(target).await?; - let quote_response: QuoteResponse = serde_json::from_slice(&response).map_err(|e| AlienError::ResponseError { msg: e.to_string() })?; - Ok(quote_response) - } - pub async fn get_swap_quote_data(&self, request: QuoteDataRequest) -> Result { - let headers = HashMap::from([("Content-Type".into(), "application/json".into())]); - let json = serde_json::to_string(&request).map_err(|e| AlienError::RequestError { msg: e.to_string() })?; - let target = AlienTarget { - url: format!("{}/swap/v1/swap", self.api_url), - method: AlienHttpMethod::Post, - headers: Some(headers), - body: Some(json.as_bytes().into()), - }; - let response = self.provider.request(target).await?; - let quote_response: QuoteDataResponse = serde_json::from_slice(&response).map_err(|e| AlienError::ResponseError { msg: e.to_string() })?; - Ok(quote_response) - } -} diff --git a/gemstone/src/swapper/pancakeswap_aptos/client.rs b/gemstone/src/swapper/pancakeswap_aptos/client.rs deleted file mode 100644 index ffadf2f53..000000000 --- a/gemstone/src/swapper/pancakeswap_aptos/client.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{ - network::{AlienProvider, AlienTarget}, - swapper::SwapperError, -}; -use gem_aptos::models::Resource; -use num_bigint::BigUint; -use std::{str::FromStr, sync::Arc}; - -use super::model::{PANCAKE_SWAP_APTOS_ADDRESS, TokenPairReserve}; - -#[derive(Debug)] -pub struct PancakeSwapAptosClient { - provider: Arc, -} - -impl PancakeSwapAptosClient { - pub fn new(provider: Arc) -> Self { - Self { provider } - } - - fn calculate_swap_output(reserve_in: BigUint, reserve_out: BigUint, amount_in: BigUint, fee_bps: u32) -> BigUint { - // Constants for basis points calculation - let bps_base = BigUint::from(10_000u32); // 10,000 bps = 100% - - // Effective input after fee deduction - let effective_fee = bps_base.clone() - BigUint::from(fee_bps); - let effective_amount_in = &amount_in * effective_fee / &bps_base; - - // Calculate numerator and denominator - let numerator = &reserve_out * &effective_amount_in; - let denominator = &reserve_in + &effective_amount_in; - - // Final output - numerator / denominator - } - - fn sort_assets(&self, asset1: T, asset2: T) -> (T, T) { - if asset1 <= asset2 { (asset1, asset2) } else { (asset2, asset1) } - } - - pub async fn get_quote(&self, endpoint: &str, from_asset: &str, to_asset: &str, value: &str, slippage_bps: u32) -> Result { - let (asset1, asset2) = self.sort_assets(from_asset, to_asset); - let address = PANCAKE_SWAP_APTOS_ADDRESS; - let resource = format!("{address}::swap::TokenPairReserve<{asset1}, {asset2}>"); - let path = format!("/v1/accounts/{address}/resource/{resource}"); - let url = format!("{endpoint}{path}"); - - let target = AlienTarget::get(&url); - let data = self.provider.request(target).await.map_err(SwapperError::from)?; - - let result: Resource = serde_json::from_slice(&data).map_err(|e| SwapperError::NetworkError(e.to_string()))?; - - let reserve_x = BigUint::from_str(result.data.reserve_x.as_str()).unwrap_or_default(); - let reserve_y = BigUint::from_str(result.data.reserve_y.as_str()).unwrap_or_default(); - - let reserve_in = if asset1 == from_asset { reserve_x.clone() } else { reserve_y.clone() }; - let reserve_out = if asset1 == from_asset { reserve_y.clone() } else { reserve_x.clone() }; - let amount_in = BigUint::from_str(value).unwrap_or_default(); - - let value = Self::calculate_swap_output(reserve_in, reserve_out, amount_in, slippage_bps); - - Ok(value.to_string()) - } -} diff --git a/gemstone/src/swapper/proxy/client.rs b/gemstone/src/swapper/proxy/client.rs deleted file mode 100644 index bfd401b31..000000000 --- a/gemstone/src/swapper/proxy/client.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{ - network::{AlienProvider, AlienTarget}, - swapper::SwapperError, -}; -use primitives::swap::{ProxyQuote, ProxyQuoteRequest, SwapQuoteData}; -use serde::Deserialize; -use std::sync::Arc; - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum ProxyResult { - Ok(T), - Err { error: String }, -} - -#[derive(Debug)] -pub struct ProxyClient { - provider: Arc, -} - -impl ProxyClient { - pub fn new(provider: Arc) -> Self { - Self { provider } - } - - pub async fn get_quote(&self, endpoint: &str, request: ProxyQuoteRequest) -> Result { - let url = format!("{endpoint}/quote"); - let target = AlienTarget::post_json(&url, serde_json::json!(request)); - let data = self.provider.request(target).await.map_err(SwapperError::from)?; - - match serde_json::from_slice::>(&data).map_err(SwapperError::from)? { - ProxyResult::Ok(q) => Ok(q), - ProxyResult::Err { error } => Err(SwapperError::ComputeQuoteError(error)), - } - } - - pub async fn get_quote_data(&self, endpoint: &str, quote: ProxyQuote) -> Result { - let url = format!("{endpoint}/quote_data"); - let target = AlienTarget::post_json(&url, serde_json::json!(quote)); - - let data = self.provider.request(target).await.map_err(SwapperError::from)?; - - match serde_json::from_slice::>(&data).map_err(SwapperError::from)? { - ProxyResult::Ok(qd) => Ok(qd), - ProxyResult::Err { error } => Err(SwapperError::TransactionError(error)), - } - } -} diff --git a/gemstone/src/swapper/proxy/provider_factory.rs b/gemstone/src/swapper/proxy/provider_factory.rs deleted file mode 100644 index 70e866d6b..000000000 --- a/gemstone/src/swapper/proxy/provider_factory.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::swapper::{SwapperProvider, SwapperProviderType, asset::*, models::SwapperChainAsset}; -use primitives::Chain; - -use super::provider::{PROVIDER_API_URL, ProxyProvider}; - -impl ProxyProvider { - pub fn new_stonfi_v2() -> ProxyProvider { - ProxyProvider { - provider: SwapperProviderType::new(SwapperProvider::StonfiV2), - url: format!("{}/{}", PROVIDER_API_URL, "stonfi_v2"), - assets: vec![SwapperChainAsset::All(Chain::Ton)], - } - } - - pub fn new_symbiosis() -> ProxyProvider { - ProxyProvider { - provider: SwapperProviderType::new(SwapperProvider::Symbiosis), - url: format!("{}/{}", PROVIDER_API_URL, "symbiosis"), - assets: vec![SwapperChainAsset::All(Chain::Tron)], - } - } - - pub fn new_cetus_aggregator() -> ProxyProvider { - ProxyProvider { - provider: SwapperProviderType::new(SwapperProvider::CetusAggregator), - url: format!("{}/{}", PROVIDER_API_URL, "cetus"), - assets: vec![SwapperChainAsset::All(Chain::Sui)], - } - } - - pub fn new_mayan() -> ProxyProvider { - ProxyProvider { - provider: SwapperProviderType::new(SwapperProvider::Mayan), - url: format!("{}/{}", PROVIDER_API_URL, "mayan"), - assets: vec![ - SwapperChainAsset::Assets( - Chain::Ethereum, - vec![ - ETHEREUM_USDT.id.clone(), - ETHEREUM_USDC.id.clone(), - ETHEREUM_DAI.id.clone(), - ETHEREUM_USDS.id.clone(), - ETHEREUM_WBTC.id.clone(), - ETHEREUM_WETH.id.clone(), - ETHEREUM_STETH.id.clone(), - ETHEREUM_CBBTC.id.clone(), - ], - ), - SwapperChainAsset::Assets( - Chain::Solana, - vec![ - SOLANA_USDC.id.clone(), - SOLANA_USDT.id.clone(), - SOLANA_USDS.id.clone(), - SOLANA_CBBTC.id.clone(), - SOLANA_WBTC.id.clone(), - SOLANA_JITO_SOL.id.clone(), - ], - ), - SwapperChainAsset::Assets(Chain::Sui, vec![SUI_USDC.id.clone(), SUI_SBUSDT.id.clone(), SUI_WAL.id.clone()]), - SwapperChainAsset::Assets( - Chain::SmartChain, - vec![SMARTCHAIN_USDT.id.clone(), SMARTCHAIN_USDC.id.clone(), SMARTCHAIN_WBTC.id.clone()], - ), - SwapperChainAsset::Assets( - Chain::Base, - vec![BASE_USDC.id.clone(), BASE_CBBTC.id.clone(), BASE_WBTC.id.clone(), BASE_USDS.id.clone()], - ), - SwapperChainAsset::Assets(Chain::Polygon, vec![POLYGON_USDC.id.clone(), POLYGON_USDT.id.clone()]), - SwapperChainAsset::Assets(Chain::AvalancheC, vec![AVALANCHE_USDT.id.clone(), AVALANCHE_USDC.id.clone()]), - SwapperChainAsset::Assets(Chain::Arbitrum, vec![ARBITRUM_USDC.id.clone(), ARBITRUM_USDT.id.clone()]), - SwapperChainAsset::Assets(Chain::Optimism, vec![OPTIMISM_USDC.id.clone(), OPTIMISM_USDT.id.clone()]), - SwapperChainAsset::Assets(Chain::Linea, vec![LINEA_USDC.id.clone(), LINEA_USDT.id.clone()]), - SwapperChainAsset::Assets(Chain::Unichain, vec![UNICHAIN_USDC.id.clone(), UNICHAIN_DAI.id.clone()]), - ], - } - } - - pub fn new_relay() -> ProxyProvider { - ProxyProvider { - provider: SwapperProviderType::new(SwapperProvider::Relay), - url: format!("{}/{}", PROVIDER_API_URL, "relay"), - assets: vec![ - SwapperChainAsset::All(Chain::Hyperliquid), - SwapperChainAsset::All(Chain::Manta), - SwapperChainAsset::All(Chain::Berachain), - ], - } - } -} diff --git a/gemstone/src/swapper/remote_models.rs b/gemstone/src/swapper/remote_models.rs deleted file mode 100644 index 73af9c3e5..000000000 --- a/gemstone/src/swapper/remote_models.rs +++ /dev/null @@ -1,85 +0,0 @@ -use primitives::Chain; - -use crate::models::GemApprovalData; - -pub type SwapperProvider = primitives::SwapProvider; -pub type SwapperProviderMode = primitives::swap::SwapProviderMode; -pub type SwapperQuoteAsset = primitives::swap::QuoteAsset; -pub type SwapperMode = primitives::swap::SwapMode; -pub type SwapperSlippage = primitives::swap::Slippage; -pub type SwapperSlippageMode = primitives::swap::SlippageMode; -pub type SwapperQuoteData = primitives::swap::SwapQuoteData; -pub type SwapperSwapStatus = primitives::swap::SwapStatus; - -#[uniffi::remote(Record)] -pub struct SwapperQuoteData { - pub to: String, - pub value: String, - pub data: String, - pub approval: Option, - pub gas_limit: Option, -} - -#[uniffi::remote(Enum)] -pub enum SwapperProvider { - UniswapV3, - UniswapV4, - PancakeswapV3, - Aerodrome, - PancakeswapAptosV2, - Thorchain, - Jupiter, - Across, - Oku, - Wagmi, - Cetus, - StonfiV2, - Mayan, - Reservoir, - Symbiosis, - Chainflip, - CetusAggregator, - Relay, - Hyperliquid, -} - -#[uniffi::remote(Enum)] -pub enum SwapperProviderMode { - OnChain, - CrossChain, - Bridge, - OmniChain(Vec), -} - -#[uniffi::remote(Enum)] -pub enum SwapperMode { - ExactIn, - ExactOut, -} - -#[uniffi::remote(Record)] -pub struct SwapperSlippage { - pub bps: u32, - pub mode: SwapperSlippageMode, -} - -#[uniffi::remote(Enum)] -pub enum SwapperSlippageMode { - Auto, - Exact, -} - -#[uniffi::remote(Record)] -pub struct SwapperQuoteAsset { - pub id: String, - pub symbol: String, - pub decimals: u32, -} - -#[uniffi::remote(Enum)] -pub enum SwapperSwapStatus { - Pending, - Completed, - Failed, - Refunded, -} diff --git a/gemstone/src/swapper/thorchain/client.rs b/gemstone/src/swapper/thorchain/client.rs deleted file mode 100644 index ca4b8cb3c..000000000 --- a/gemstone/src/swapper/thorchain/client.rs +++ /dev/null @@ -1,68 +0,0 @@ -use super::{ - asset::THORChainAsset, - model::{InboundAddress, QuoteSwapRequest, QuoteSwapResponse, Transaction}, -}; -use crate::network::{AlienHttpMethod, AlienProvider, AlienTarget, X_CACHE_TTL}; -use crate::swapper::SwapperError; -use std::{collections::HashMap, sync::Arc}; - -#[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: THORChainAsset, - to_asset: THORChainAsset, - value: String, - streaming_interval: i64, - streaming_quantity: i64, - affiliate: String, - affiliate_bps: i64, - ) -> Result { - let params = QuoteSwapRequest { - from_asset: from_asset.asset_name(), - to_asset: to_asset.asset_name(), - amount: value.clone(), - affiliate, - affiliate_bps, - streaming_interval, - streaming_quantity, - }; - let query = serde_urlencoded::to_string(params).unwrap(); - let target = AlienTarget::get(format!("{}{}?{}", endpoint, "/thorchain/quote/swap", query).as_str()); - - let data = self.provider.request(target).await.map_err(SwapperError::from)?; - - serde_json::from_slice(&data).map_err(SwapperError::from) - } - - #[allow(dead_code)] - pub async fn get_inbound_addresses(&self, endpoint: &str) -> Result, SwapperError> { - let target = AlienTarget { - url: format!("{endpoint}/thorchain/inbound_addresses"), - method: AlienHttpMethod::Get, - headers: Some(HashMap::from([(X_CACHE_TTL.into(), "600".into())])), - body: None, - }; - - let data = self.provider.request(target).await.map_err(SwapperError::from)?; - - serde_json::from_slice(&data).map_err(SwapperError::from) - } - - pub async fn get_transaction_status(&self, endpoint: &str, transaction_hash: &str) -> Result { - let target = AlienTarget::get(format!("{endpoint}/thorchain/tx/{transaction_hash}").as_str()); - - let data = self.provider.request(target).await.map_err(SwapperError::from)?; - - serde_json::from_slice(&data).map_err(SwapperError::from) - } -} diff --git a/gemstone/src/swapper/uniswap/universal_router/mod.rs b/gemstone/src/swapper/uniswap/universal_router/mod.rs deleted file mode 100644 index 69d1da085..000000000 --- a/gemstone/src/swapper/uniswap/universal_router/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -mod aerodrome; -mod oku; -mod pancakeswap; -mod uniswap_v3; -mod wagmi; - -use crate::swapper::uniswap::{v3::UniswapV3, v4::UniswapV4}; - -pub fn new_uniswap_v3() -> UniswapV3 { - UniswapV3::new(Box::new(uniswap_v3::UniswapUniversalRouter::default())) -} - -pub fn new_pancakeswap() -> UniswapV3 { - UniswapV3::new(Box::new(pancakeswap::PancakeSwapUniversalRouter::default())) -} - -pub fn new_aerodrome() -> UniswapV3 { - UniswapV3::new(Box::new(aerodrome::AerodromeUniversalRouter::default())) -} - -pub fn new_oku() -> UniswapV3 { - UniswapV3::new(Box::new(oku::OkuUniversalRouter::default())) -} - -pub fn new_wagmi() -> UniswapV3 { - UniswapV3::new(Box::new(wagmi::WagmiUniversalRouter::default())) -} - -pub fn new_uniswap_v4() -> UniswapV4 { - UniswapV4::default() -} diff --git a/gemstone/src/tron/client.rs b/gemstone/src/tron/client.rs deleted file mode 100644 index 1dc61d472..000000000 --- a/gemstone/src/tron/client.rs +++ /dev/null @@ -1,99 +0,0 @@ -use super::{bs58_to_hex, encode_parameters, hex_to_utf8, model::*}; -use crate::network::{AlienProvider, AlienTarget}; -use crate::swapper::SwapperError; -use alloy_primitives::U256; -use primitives::Chain; -use std::sync::Arc; - -#[derive(Debug)] -pub struct TronClient { - provider: Arc, -} - -impl TronClient { - pub fn new(provider: Arc) -> Self { - Self { provider } - } - - pub async fn get_token_allowance(&self, owner_address: &str, token_address: &str, spender_address: &str) -> Result { - let owner_hex = bs58_to_hex(owner_address)?; - let spender_hex = bs58_to_hex(spender_address)?; - let parameter = encode_parameters(&owner_hex, &spender_hex); - let params = serde_json::json! ( - { - "owner_address": owner_address, - "contract_address": token_address, - "function_selector": "allowance(address,address)", - "parameter": hex::encode(¶meter), - "visible": true - } - ); - - let endpoint = self.provider.get_endpoint(Chain::Tron).map_err(SwapperError::from)?; - let url = format!("{endpoint}/wallet/triggerconstantcontract"); - let target = AlienTarget::post_json(&url, params); - let data = self.provider.request(target).await.map_err(SwapperError::from)?; - let response: TronNodeResponse = serde_json::from_slice(&data).map_err(SwapperError::from)?; - - match response.result { - TronNodeResult::Result(TronResult { result }) => { - if !result { - return Err(SwapperError::NetworkError("Check approval failed. result is false".into())); - } - let constant_result = response - .constant_result - .first() - .ok_or_else(|| SwapperError::ABIError("Missing constant_result in TronGrid response".into()))?; - let allowance = U256::from_str_radix(constant_result, 16).map_err(SwapperError::from)?; - Ok(allowance) - } - TronNodeResult::Error(TronErrorResult { code, message }) => { - let msg = format!("Check approval failed. Code: {}, Message: {}", code, hex_to_utf8(&message).unwrap_or_default()); - Err(SwapperError::NetworkError(msg)) - } - } - } - - pub async fn estimate_energy( - &self, - owner_address: &str, - contract_address: &str, - function_selector: &str, - parameter: &str, - fee_limit: u64, - call_value: &str, - ) -> Result { - let params = serde_json::json! ( - { - "owner_address": owner_address, - "contract_address": contract_address, - "function_selector": function_selector, - "parameter": parameter, - "fee_limit": fee_limit, - "call_value": call_value.parse::().unwrap_or_default(), - "visible": true - } - ); - - let endpoint = self.provider.get_endpoint(Chain::Tron).map_err(SwapperError::from)?; - let url = format!("{endpoint}/wallet/triggerconstantcontract"); - let target = AlienTarget::post_json(&url, params); - let data = self.provider.request(target).await.map_err(SwapperError::from)?; - - let response: TronNodeResponse = serde_json::from_slice(&data).map_err(|e| SwapperError::NetworkError(e.to_string()))?; - - match response.result { - TronNodeResult::Error(TronErrorResult { code, message }) => { - let msg = format!("Estimate energy failed. Code: {}, Message: {}", code, hex_to_utf8(&message).unwrap_or_default()); - Err(SwapperError::NetworkError(msg)) - } - TronNodeResult::Result(TronResult { result }) => { - if !result { - Err(SwapperError::NetworkError("Estimate energy failed".to_string())) - } else { - Ok(response.energy_used + response.energy_penalty) - } - } - } - } -} diff --git a/gemstone/src/tron/mod.rs b/gemstone/src/tron/mod.rs deleted file mode 100644 index 11c58b27d..000000000 --- a/gemstone/src/tron/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -pub mod client; -pub mod model; - -use alloy_primitives::Address; -use alloy_sol_types::SolCall; -use gem_evm::contracts::erc20::IERC20; - -use crate::swapper::SwapperError; - -pub fn bs58_to_hex(address: &str) -> Result, SwapperError> { - bs58::decode(address) - .with_check(None) - .into_vec() - .map_err(|e| SwapperError::InvalidAddress(format!("Failed to decode address '{address}': {e}"))) -} - -pub fn hex_to_utf8(hex: &str) -> Option { - hex::decode(hex).ok().and_then(|bytes| String::from_utf8(bytes).ok()) -} - -pub fn encode_parameters(owner: &[u8], spender: &[u8]) -> Vec { - let owner_addr = Address::from_slice(&owner[1..]); - let spender_addr = Address::from_slice(&spender[1..]); - let parameter = IERC20::allowanceCall { - owner: owner_addr, - spender: spender_addr, - } - .abi_encode(); - parameter[4..].to_vec() // drop function selector -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_tron_encoding() { - let token_address = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; - let owner_address = "TA7mCjHFfo68FG3wc6pDCeRGbJSPZkBfL7"; - let gateway_address = "TQjjYNyBmzCyDh5WumFJBhXFyE5PUKqVYZ"; - - let token_hex = bs58_to_hex(token_address).unwrap(); - let owner_hex = bs58_to_hex(owner_address).unwrap(); - let gateway_hex = bs58_to_hex(gateway_address).unwrap(); - - assert_eq!(hex::encode(&token_hex), "41a614f803b6fd780986a42c78ec9c7f77e6ded13c"); - assert_eq!(hex::encode(&owner_hex), "41019e353a35efaa8e27c2a602a791ae1b19d9c9fa"); - assert_eq!(hex::encode(&gateway_hex), "41a1fd8e8afc126545d76b4a9e905d5be1ccd392e1"); - - let parameter = encode_parameters(&owner_hex, &gateway_hex); - - assert_eq!( - hex::encode(¶meter), - "000000000000000000000000019e353a35efaa8e27c2a602a791ae1b19d9c9fa000000000000000000000000a1fd8e8afc126545d76b4a9e905d5be1ccd392e1" - ); - } -} diff --git a/gemstone/src/tron/model.rs b/gemstone/src/tron/model.rs deleted file mode 100644 index f262983f1..000000000 --- a/gemstone/src/tron/model.rs +++ /dev/null @@ -1,27 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct TronNodeResponse { - pub result: TronNodeResult, - pub energy_used: u64, - pub energy_penalty: u64, - pub constant_result: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -pub enum TronNodeResult { - Result(TronResult), - Error(TronErrorResult), -} - -#[derive(Deserialize, Debug)] -pub struct TronResult { - pub result: bool, -} - -#[derive(Deserialize, Debug)] -pub struct TronErrorResult { - pub code: String, - pub message: String, -} diff --git a/gemstone/tests/ios/GemTest/GemTest/ContentView.swift b/gemstone/tests/ios/GemTest/GemTest/ContentView.swift index 009e2426b..a2b762c72 100644 --- a/gemstone/tests/ios/GemTest/GemTest/ContentView.swift +++ b/gemstone/tests/ios/GemTest/GemTest/ContentView.swift @@ -72,15 +72,6 @@ struct ContentView: View { Button("Bridge Base USDC -> ETH") { self.testQuote(quote: .baseUSDC2Eth) } - Text("Solana Pay:") - Button("Paste URI") { - guard let text = UIPasteboard.general.string else { - return - } - Task { - try await self.model.fetchSolanaPay(uri: text) - } - } } .padding() .onAppear {} diff --git a/gemstone/tests/ios/GemTest/GemTest/Extension/Gemstone+Extension.swift b/gemstone/tests/ios/GemTest/GemTest/Extension/Gemstone+Extension.swift index ae621b1d6..2706f0e02 100644 --- a/gemstone/tests/ios/GemTest/GemTest/Extension/Gemstone+Extension.swift +++ b/gemstone/tests/ios/GemTest/GemTest/Extension/Gemstone+Extension.swift @@ -21,5 +21,3 @@ extension AlienTarget: URLRequestConvertible { return request } } - -extension SolanaPay: @unchecked @retroactive Sendable {} diff --git a/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift b/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift index 6cddb7a48..5ceebbf15 100644 --- a/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift +++ b/gemstone/tests/ios/GemTest/GemTest/ViewModel.swift @@ -52,24 +52,6 @@ public struct ViewModel: Sendable { print("<== getProviders:\n", swapper.getProviders()) } - public func fetchSolanaPay(uri: String) async throws { - let wrapper = try paymentDecodeUrl(string: uri) - guard let url = wrapper.paymentLink else { - print("invalid url") - return - } - do { - let solanaPay = SolanaPay(provider: self.provider) - async let labelCall = solanaPay.getLabel(link: url) - async let txCall = solanaPay.postAccount(link: url, account: TEST_SOL_WALLET) - - let (label, tx) = try await (labelCall, txCall) - print(label, tx) - } catch { - print(error) - } - } - func dumpQuote(_ quote: SwapperQuote) { let route = quote.data.routes.first! print("<== fetchQuote:\n", quote.description) diff --git a/justfile b/justfile index 830655d14..0740c948a 100644 --- a/justfile +++ b/justfile @@ -54,6 +54,7 @@ build-integration-tests: cargo test --no-run --test integration_test --package gem_evm --features rpc,reqwest cargo test --no-run --test integration_test --package security_provider cargo test --no-run --test integration_test --package name_resolver + cargo test --no-run --test integration_test --package swapper --features swap_integration_tests format: cargo fmt -q --all