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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/gem_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ edition = "2021"
[features]
default = []
reqwest = ["dep:reqwest", "dep:tokio"]
testkit = []

[dependencies]
async-trait = { workspace = true }
Expand Down
9 changes: 6 additions & 3 deletions crates/gem_client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
mod content_type;
mod types;

#[cfg(feature = "testkit")]
pub mod testkit;

#[cfg(feature = "reqwest")]
mod reqwest_client;

Expand All @@ -12,9 +15,9 @@ pub mod client_config;

pub mod query;

pub use content_type::{ContentType, CONTENT_TYPE};
pub use content_type::{CONTENT_TYPE, ContentType};
pub use query::build_path_with_query;
pub use types::{decode_json_byte_array, deserialize_response, ClientError, Response};
pub use types::{ClientError, Response, decode_json_byte_array, deserialize_response};

#[cfg(feature = "reqwest")]
pub use reqwest_client::ReqwestClient;
Expand All @@ -26,7 +29,7 @@ pub use retry::{default_should_retry, retry, retry_policy};
pub use client_config::builder;

use async_trait::async_trait;
use serde::{de::DeserializeOwned, Serialize};
use serde::{Serialize, de::DeserializeOwned};
use std::{collections::HashMap, fmt::Debug};

pub type Data = Vec<u8>;
Expand Down
6 changes: 3 additions & 3 deletions crates/gem_client/src/reqwest_client.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::{deserialize_response, retry_policy, Client, ClientError, ContentType, Response, CONTENT_TYPE};
use crate::{CONTENT_TYPE, Client, ClientError, ContentType, Response, deserialize_response, retry_policy};
use async_trait::async_trait;
use reqwest::header::USER_AGENT;
use reqwest::RequestBuilder;
use serde::{de::DeserializeOwned, Serialize};
use reqwest::header::USER_AGENT;
use serde::{Serialize, de::DeserializeOwned};
use std::{collections::HashMap, str::FromStr, time::Duration};

#[derive(Debug, Clone)]
Expand Down
2 changes: 1 addition & 1 deletion crates/gem_client/src/retry.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use reqwest::{retry, StatusCode};
use reqwest::{StatusCode, retry};
use std::future::Future;
use std::time::Duration;

Expand Down
68 changes: 68 additions & 0 deletions crates/gem_client/src/testkit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::{Client, ClientError};
use async_trait::async_trait;
use serde::{de::DeserializeOwned, Serialize};
use std::{
collections::HashMap,
fmt::{Debug, Formatter},
sync::Arc,
};

type GetHandler = Arc<dyn Fn(&str) -> Result<Vec<u8>, ClientError> + Send + Sync>;
type PostHandler = Arc<dyn Fn(&str, &[u8]) -> Result<Vec<u8>, ClientError> + Send + Sync>;

#[derive(Clone, Default)]
pub struct MockClient {
get_handler: Option<GetHandler>,
post_handler: Option<PostHandler>,
}

impl MockClient {
pub fn new() -> Self {
Self::default()
}

pub fn with_get<F>(mut self, handler: F) -> Self
where
F: Fn(&str) -> Result<Vec<u8>, ClientError> + Send + Sync + 'static,
{
self.get_handler = Some(Arc::new(handler));
self
}

pub fn with_post<F>(mut self, handler: F) -> Self
where
F: Fn(&str, &[u8]) -> Result<Vec<u8>, ClientError> + Send + Sync + 'static,
{
self.post_handler = Some(Arc::new(handler));
self
}
}

impl Debug for MockClient {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MockClient").finish()
}
}

#[async_trait]
impl Client for MockClient {
async fn get<R>(&self, path: &str) -> Result<R, ClientError>
where
R: DeserializeOwned,
{
let handler = self.get_handler.as_ref().ok_or(ClientError::Http { status: 404, body: vec![] })?;
let bytes = handler(path)?;
serde_json::from_slice(&bytes).map_err(|e| ClientError::Serialization(e.to_string()))
}

async fn post<T, R>(&self, path: &str, body: &T, _headers: Option<HashMap<String, String>>) -> Result<R, ClientError>
where
T: Serialize + Send + Sync,
R: DeserializeOwned,
{
let handler = self.post_handler.as_ref().ok_or(ClientError::Http { status: 404, body: vec![] })?;
let body_bytes = serde_json::to_vec(body).map_err(|e| ClientError::Serialization(e.to_string()))?;
let bytes = handler(path, &body_bytes)?;
serde_json::from_slice(&bytes).map_err(|e| ClientError::Serialization(e.to_string()))
}
}
76 changes: 28 additions & 48 deletions crates/gem_solana/src/provider/preload_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ fn get_gas_limit(input_type: &TransactionInputType) -> BigInt {
| TransactionInputType::TokenApprove(_, _)
| TransactionInputType::Generic(_, _, _)
| TransactionInputType::Perpetual(_, _) => BigInt::from(100_000),
TransactionInputType::Swap(_, _, _) => BigInt::from(420_000),
TransactionInputType::Swap(_, _, swap_data) => swap_data
.data
.gas_limit
.as_ref()
.and_then(|x| x.parse::<u64>().ok())
.map(BigInt::from)
.unwrap_or(BigInt::from(420_000)),
TransactionInputType::Stake(_, _) => BigInt::from(100_000),
}
}
Expand Down Expand Up @@ -112,9 +118,14 @@ mod tests {
use primitives::swap::SwapData;
use primitives::{Asset, AssetId, AssetType, Chain, SwapProvider};

fn mock_swap_data_with_gas_limit(provider: SwapProvider, gas_limit: Option<&str>) -> SwapData {
let mut data = SwapData::mock_with_provider(provider);
data.data.gas_limit = gas_limit.map(|s| s.to_string());
data
}

#[test]
fn test_calculate_transaction_fee() {
// Test with EIP-1559 gas pricing
let gas_price_type = GasPriceType::eip1559(BigInt::from(5000u64), BigInt::from(15000u64));
let input_type = TransactionInputType::Transfer(Asset {
id: AssetId::from_chain(Chain::Solana),
Expand All @@ -137,36 +148,25 @@ mod tests {

#[test]
fn test_calculate_transaction_fee_swap() {
// Test swap transaction with higher gas limit
let gas_price_type = GasPriceType::eip1559(BigInt::from(5000u64), BigInt::from(30000u64));
let input_type = TransactionInputType::Swap(
Asset {
id: AssetId::from_chain(Chain::Solana),
chain: Chain::Solana,
token_id: None,
name: "SOL".to_string(),
symbol: "SOL".to_string(),
decimals: 9,
asset_type: AssetType::NATIVE,
},
Asset {
id: AssetId::from_chain(Chain::Solana),
chain: Chain::Solana,
token_id: Some("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string()),
name: "USDC".to_string(),
symbol: "USDC".to_string(),
decimals: 6,
asset_type: AssetType::SPL,
},
SwapData::mock_with_provider(SwapProvider::Jupiter),
);
let gas_price_type = GasPriceType::solana(5000u64, 30000u64, 100u64);
let input_type = TransactionInputType::Swap(Asset::mock_sol(), Asset::mock_spl_token(), mock_swap_data_with_gas_limit(SwapProvider::Jupiter, None));

let fee = calculate_transaction_fee(&input_type, &gas_price_type, None);
let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("recipient_token_address".to_string()));

assert_eq!(fee.fee, BigInt::from(35_000u64));
assert_eq!(fee.gas_limit, BigInt::from(420_000u64));
}

#[test]
fn test_calculate_transaction_fee_swap_with_provider_gas_limit() {
let gas_price_type = GasPriceType::solana(5000u64, 30000u64, 100u64);
let input_type = TransactionInputType::Swap(Asset::mock_sol(), Asset::mock_spl_token(), mock_swap_data_with_gas_limit(SwapProvider::Okx, Some("550000")));

let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("recipient_token_address".to_string()));

assert_eq!(fee.gas_limit, BigInt::from(550_000u64));
}

#[test]
fn test_calculate_transaction_fee_cross_chain_swap_without_token_creation() {
let gas_price_type = GasPriceType::eip1559(BigInt::from(5000u64), BigInt::from(15000u64));
Expand Down Expand Up @@ -273,27 +273,7 @@ mod tests {
#[test]
fn test_calculate_fee_rates_swap() {
let fees = vec![SolanaPrioritizationFee { prioritization_fee: 150_000 }];
let input_type = TransactionInputType::Swap(
Asset {
id: AssetId::from_chain(Chain::Solana),
chain: Chain::Solana,
token_id: None,
name: "SOL".to_string(),
symbol: "SOL".to_string(),
decimals: 9,
asset_type: AssetType::NATIVE,
},
Asset {
id: AssetId::from_chain(Chain::Solana),
chain: Chain::Solana,
token_id: Some("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string()),
name: "USDC".to_string(),
symbol: "USDC".to_string(),
decimals: 6,
asset_type: AssetType::SPL,
},
SwapData::mock_with_provider(SwapProvider::Jupiter),
);
let input_type = TransactionInputType::Swap(Asset::mock_sol(), Asset::mock_spl_token(), mock_swap_data_with_gas_limit(SwapProvider::Jupiter, None));

let rates = calculate_fee_rates(&input_type, &fees);
assert_eq!(rates.len(), 3);
Expand Down Expand Up @@ -365,7 +345,7 @@ mod tests {
};
let input_type = TransactionInputType::Transfer(asset);

let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("existing_account".to_string()));
let fee = calculate_transaction_fee(&input_type, &gas_price_type, Some("recipient_token_address".to_string()));

assert_eq!(fee.fee, BigInt::from(20_000u64));
assert!(fee.options.is_empty());
Expand Down
7 changes: 7 additions & 0 deletions crates/primitives/src/testkit/swap_mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ impl SwapQuoteData {
gas_limit: Some("21000".to_string()),
}
}

pub fn mock_with_gas_limit(gas_limit: Option<String>) -> Self {
SwapQuoteData {
gas_limit,
..Self::mock()
}
}
}

impl SwapProviderData {
Expand Down
2 changes: 2 additions & 0 deletions crates/swapper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ tracing = "0.1.44"

[dev-dependencies]
tokio.workspace = true
primitives = { path = "../primitives", features = ["testkit"], default-features = false }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default-features = false should not be here

gem_client = { path = "../gem_client", features = ["testkit"] }
52 changes: 48 additions & 4 deletions crates/swapper/src/proxy/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ where
}
}

pub async fn check_approval(&self, quote: &Quote, quote_data: &SwapQuoteData) -> Result<(Option<ApprovalData>, Option<String>), SwapperError> {
pub async fn check_approval_and_limit(&self, quote: &Quote, quote_data: &SwapQuoteData) -> Result<(Option<ApprovalData>, Option<String>), SwapperError> {
let request = &quote.request;
let from_asset = request.from_asset.asset_id();

Expand All @@ -66,8 +66,7 @@ where
.await
}
}
ChainType::Tron => Ok((None, None)),
_ => Ok((None, None)),
_ => Ok((None, quote_data.gas_limit.clone())),
}
}

Expand Down Expand Up @@ -222,7 +221,7 @@ where
let route_data: ProxyQuote = serde_json::from_str(&routes.first().unwrap().route_data).map_err(|_| SwapperError::InvalidRoute)?;

let data = self.client.get_quote_data(route_data).await?;
let (approval, gas_limit) = self.check_approval(quote, &data).await?;
let (approval, gas_limit) = self.check_approval_and_limit(quote, &data).await?;

Ok(SwapperQuoteData::new_contract(data.to, data.value, data.data, approval, gas_limit))
}
Expand Down Expand Up @@ -261,6 +260,51 @@ where
}
}

#[cfg(test)]
mod tests {
use super::super::client::ProxyClient;
use super::*;
use crate::alien::mock::ProviderMock;
use gem_client::testkit::MockClient;
use primitives::swap::SwapQuoteData;

fn mock_provider(provider: SwapperProvider) -> ProxyProvider<MockClient> {
let rpc_provider = Arc::new(ProviderMock::new("{}".to_string()));
ProxyProvider::new_with_client(provider, ProxyClient::new(MockClient::new()), vec![], rpc_provider)
}

#[tokio::test]
async fn test_solana_preserves_provider_gas_limit() {
let provider = mock_provider(SwapperProvider::Okx);
let quote = Quote::mock(Chain::Solana, None);
let data = SwapQuoteData::mock_with_gas_limit(Some("550000".to_string()));

let (approval, gas_limit) = provider.check_approval_and_limit(&quote, &data).await.unwrap();

assert!(approval.is_none());
assert_eq!(gas_limit, Some("550000".to_string()));

let data = SwapQuoteData::mock_with_gas_limit(None);

let (approval, gas_limit) = provider.check_approval_and_limit(&quote, &data).await.unwrap();

assert!(approval.is_none());
assert!(gas_limit.is_none());
}

#[tokio::test]
async fn test_evm_native_ignores_provider_gas_limit() {
let provider = mock_provider(SwapperProvider::Mayan);
let quote = Quote::mock(Chain::Ethereum, None);
let data = SwapQuoteData::mock_with_gas_limit(Some("550000".to_string()));

let (approval, gas_limit) = provider.check_approval_and_limit(&quote, &data).await.unwrap();

assert!(approval.is_none());
assert!(gas_limit.is_none());
}
}

#[cfg(all(test, feature = "swap_integration_tests"))]
mod swap_integration_tests {
use super::*;
Expand Down
42 changes: 39 additions & 3 deletions crates/swapper/src/testkit.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,48 @@
use crate::{
FetchQuoteData, ProviderType, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, SwapperSlippage, SwapperSlippageMode,
config::get_swap_config,
FetchQuoteData, ProviderData, ProviderType, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, SwapperSlippage,
SwapperSlippageMode, config::get_swap_config,
};
use async_trait::async_trait;
use primitives::Chain;
use primitives::{AssetId, Chain};

use super::{Options, Quote, QuoteRequest, SwapperMode};

impl ProviderData {
pub fn mock() -> Self {
ProviderData {
provider: ProviderType::new(SwapperProvider::Okx),
routes: vec![],
slippage_bps: 50,
}
}
}

impl QuoteRequest {
pub fn mock(chain: Chain, token_id: Option<&str>) -> Self {
QuoteRequest {
from_asset: SwapperQuoteAsset::from(AssetId::from(chain, token_id.map(|s| s.to_string()))),
to_asset: SwapperQuoteAsset::from(AssetId::from_chain(chain)),
wallet_address: "address".to_string(),
destination_address: "address".to_string(),
value: "1000000".to_string(),
mode: SwapperMode::ExactIn,
options: Options::default(),
}
}
}

impl Quote {
pub fn mock(chain: Chain, token_id: Option<&str>) -> Self {
Quote {
from_value: "1000000".to_string(),
to_value: "1000000".to_string(),
data: ProviderData::mock(),
request: QuoteRequest::mock(chain, token_id),
eta_in_seconds: None,
}
}
}

pub fn mock_quote(from_asset: SwapperQuoteAsset, to_asset: SwapperQuoteAsset) -> QuoteRequest {
let config = get_swap_config();

Expand Down
Loading
Loading