diff --git a/crates/gem_ton/src/provider/preload.rs b/crates/gem_ton/src/provider/preload.rs index dcc0cb5f6..fbaf1f55f 100644 --- a/crates/gem_ton/src/provider/preload.rs +++ b/crates/gem_ton/src/provider/preload.rs @@ -5,7 +5,7 @@ use num_bigint::BigInt; use primitives::FeeOption; use primitives::{ AssetSubtype, FeePriority, FeeRate, GasPriceType, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, - TransactionPreloadInput, + TransactionPreloadInput, swap::SwapQuoteDataType, }; use std::collections::HashMap; use std::error::Error; @@ -17,32 +17,29 @@ const TON_BASE_FEE: u64 = 10_000_000; const JETTON_ACCOUNT_FEE_EXISTING: u64 = 100_000_000; const JETTON_ACCOUNT_FEE_EXISTING_WITH_MEMO: u64 = 60_000_000; const JETTON_ACCOUNT_CREATION: u64 = 200_000_000; +const SWAP_NATIVE_RESERVE: u64 = 310_000_000; pub fn calculate_transaction_fee(input: &TransactionLoadInput, recipient_token_address: Option) -> TransactionFee { let base_fee = BigInt::from(TON_BASE_FEE); let mut options = HashMap::new(); let fee = match &input.input_type { - TransactionInputType::Transfer(asset) | TransactionInputType::TransferNft(asset, _) | TransactionInputType::Account(asset, _) => match asset.id.token_subtype() { - AssetSubtype::NATIVE => base_fee.clone(), - AssetSubtype::TOKEN => { - let jetton_fee = if recipient_token_address.is_some() { - if input.memo.is_some() { - BigInt::from(JETTON_ACCOUNT_FEE_EXISTING_WITH_MEMO) - } else { - BigInt::from(JETTON_ACCOUNT_FEE_EXISTING) - } - } else { - BigInt::from(JETTON_ACCOUNT_CREATION) - }; - options.insert(FeeOption::TokenAccountCreation, jetton_fee.clone()); + TransactionInputType::Transfer(asset) | TransactionInputType::TransferNft(asset, _) | TransactionInputType::Account(asset, _) => { + transfer_fee(asset.id.token_subtype(), input.memo.as_deref(), recipient_token_address.as_deref(), &base_fee, &mut options) + } + TransactionInputType::Swap(from_asset, _, swap_data) => match &swap_data.data.data_type { + SwapQuoteDataType::Contract => { + options.insert(FeeOption::TokenAccountCreation, BigInt::from(SWAP_NATIVE_RESERVE)); base_fee } + SwapQuoteDataType::Transfer => transfer_fee( + from_asset.id.token_subtype(), + input.memo.as_deref(), + recipient_token_address.as_deref(), + &base_fee, + &mut options, + ), }, - TransactionInputType::Swap(_, _, _) => { - options.insert(FeeOption::TokenAccountCreation, BigInt::from(JETTON_ACCOUNT_CREATION)); - base_fee - } TransactionInputType::TokenApprove(_, _) => base_fee.clone(), TransactionInputType::Generic(_, _, _) => base_fee.clone(), TransactionInputType::Perpetual(_, _) => base_fee.clone(), @@ -52,6 +49,25 @@ pub fn calculate_transaction_fee(input: &TransactionLoadInput, recipient_token_a TransactionFee::new_gas_price_type(GasPriceType::regular(fee.clone()), fee.clone(), BigInt::from(1), options) } +fn transfer_fee(asset_subtype: AssetSubtype, memo: Option<&str>, recipient_token_address: Option<&str>, base_fee: &BigInt, options: &mut HashMap) -> BigInt { + match asset_subtype { + AssetSubtype::NATIVE => base_fee.clone(), + AssetSubtype::TOKEN => { + let jetton_fee = if recipient_token_address.is_some() { + if memo.is_some() { + BigInt::from(JETTON_ACCOUNT_FEE_EXISTING_WITH_MEMO) + } else { + BigInt::from(JETTON_ACCOUNT_FEE_EXISTING) + } + } else { + BigInt::from(JETTON_ACCOUNT_CREATION) + }; + options.insert(FeeOption::TokenAccountCreation, jetton_fee); + base_fee.clone() + } + } +} + #[async_trait] impl ChainTransactionLoad for TonClient { async fn get_transaction_preload(&self, input: TransactionPreloadInput) -> Result> { @@ -64,13 +80,19 @@ impl ChainTransactionLoad for TonClient { let token_id = asset.id.token_id.as_ref().ok_or("Missing token ID for jetton transaction")?; let jetton_token_id = base64_to_hex_address(token_id).ok_or("Invalid jetton token ID")?.to_uppercase(); - let (sender_jetton_wallets, recipient_jetton_wallets) = futures::try_join!( - self.get_jetton_wallets(input.sender_address.clone()), - self.get_jetton_wallets(input.destination_address.clone()), //TODO: different destination address for swaps - )?; + let sender_wallets = self.get_jetton_wallets(input.sender_address.clone()); + let recipient_wallets = async { + match get_recipient_jetton_wallet(&input) { + Some(address) => self.get_jetton_wallets(address.to_string()).await.map(Some), + None => Ok(None), + } + }; + let (sender_jetton_wallets, recipient_jetton_wallets) = futures::future::try_join(sender_wallets, recipient_wallets).await?; let sender_jetton_wallet_address = sender_jetton_wallets.jetton_wallets.iter().find(|wallet| wallet.jetton == jetton_token_id); - let recipient_jetton_wallet_address = recipient_jetton_wallets.jetton_wallets.iter().find(|wallet| wallet.jetton == jetton_token_id); + let recipient_jetton_wallet_address = recipient_jetton_wallets + .as_ref() + .and_then(|wallets| wallets.jetton_wallets.iter().find(|wallet| wallet.jetton == jetton_token_id)); Ok(TransactionLoadMetadata::Ton { sender_token_address: sender_jetton_wallet_address.map(|x| x.address.clone()), @@ -99,11 +121,21 @@ impl ChainTransactionLoad for TonClient { } } +fn get_recipient_jetton_wallet(input: &TransactionPreloadInput) -> Option<&str> { + match &input.input_type { + TransactionInputType::Swap(_, _, swap_data) => match &swap_data.data.data_type { + SwapQuoteDataType::Transfer => Some(&swap_data.data.to), + SwapQuoteDataType::Contract => None, + }, + _ => Some(&input.destination_address), + } +} + #[cfg(test)] mod tests { use super::*; use num_bigint::BigInt; - use primitives::{Asset, AssetId, AssetType, Chain, GasPriceType}; + use primitives::{Asset, AssetId, AssetType, Chain, GasPriceType, SwapProvider, TransactionPreloadInput, swap::SwapData}; fn create_input(asset_type: AssetType, memo: Option) -> TransactionLoadInput { let (token_id, name, symbol, decimals) = match asset_type { @@ -183,4 +215,94 @@ mod tests { assert_eq!(fee.fee, BigInt::from(TON_BASE_FEE + JETTON_ACCOUNT_CREATION)); assert_eq!(fee.options.get(&FeeOption::TokenAccountCreation), Some(&BigInt::from(JETTON_ACCOUNT_CREATION))); } + + #[test] + fn test_swap_contract_native_fee_includes_native_reserve() { + let swap_data = SwapData::mock_contract(SwapProvider::StonfiV2, "400000000", "1000000", "710000000"); + let input = TransactionLoadInput { + input_type: TransactionInputType::Swap(Asset::from_chain(Chain::Ton), Asset::from_chain(Chain::Ton), swap_data), + value: "400000000".to_string(), + ..create_input(AssetType::NATIVE, None) + }; + + let fee = calculate_transaction_fee(&input, None); + + assert_eq!(fee.fee, BigInt::from(320000000u64)); + assert_eq!(fee.options.get(&FeeOption::TokenAccountCreation), Some(&BigInt::from(310000000u64))); + } + + #[test] + fn test_swap_contract_jetton_fee_includes_native_reserve() { + let from_asset = Asset::mock_ton_usdt(); + let swap_data = SwapData::mock_contract(SwapProvider::StonfiV2, "2000000", "400000000", "300000000"); + let input = TransactionLoadInput { + input_type: TransactionInputType::Swap(from_asset, Asset::from_chain(Chain::Ton), swap_data), + value: "2000000".to_string(), + ..create_input(AssetType::JETTON, None) + }; + + let fee = calculate_transaction_fee(&input, None); + + assert_eq!(fee.fee, BigInt::from(TON_BASE_FEE + SWAP_NATIVE_RESERVE)); + assert_eq!(fee.options.get(&FeeOption::TokenAccountCreation), Some(&BigInt::from(SWAP_NATIVE_RESERVE))); + } + + #[test] + fn test_swap_transfer_native_fee_uses_transfer_fee() { + let swap_data = SwapData::mock_transfer(SwapProvider::NearIntents, "400000000", "1000000", "ton_deposit_address"); + let input = TransactionLoadInput { + input_type: TransactionInputType::Swap(Asset::from_chain(Chain::Ton), Asset::from_chain(Chain::Near), swap_data), + value: "400000000".to_string(), + ..create_input(AssetType::NATIVE, None) + }; + + let fee = calculate_transaction_fee(&input, None); + + assert_eq!(fee.fee, BigInt::from(TON_BASE_FEE)); + assert_eq!(fee.options.len(), 0); + } + + #[test] + fn test_swap_transfer_jetton_fee_uses_token_transfer_fee() { + let swap_data = SwapData::mock_transfer(SwapProvider::NearIntents, "2000000", "400000000", "ton_deposit_address"); + let input = TransactionLoadInput { + input_type: TransactionInputType::Swap(Asset::mock_ton_usdt(), Asset::from_chain(Chain::Near), swap_data), + value: "2000000".to_string(), + ..create_input(AssetType::JETTON, None) + }; + + let fee = calculate_transaction_fee(&input, None); + + assert_eq!(fee.fee, BigInt::from(TON_BASE_FEE + JETTON_ACCOUNT_CREATION)); + assert_eq!(fee.options.get(&FeeOption::TokenAccountCreation), Some(&BigInt::from(JETTON_ACCOUNT_CREATION))); + } + + #[test] + fn test_get_recipient_jetton_wallet() { + let transfer = TransactionPreloadInput { + input_type: TransactionInputType::Transfer(Asset::mock_ton_usdt()), + sender_address: "sender".to_string(), + destination_address: "recipient".to_string(), + }; + assert_eq!(get_recipient_jetton_wallet(&transfer), Some("recipient")); + + let swap_data = SwapData::mock_transfer(SwapProvider::NearIntents, "2000000", "400000000", "ton_deposit_address"); + let transfer_swap = TransactionPreloadInput { + input_type: TransactionInputType::Swap(Asset::mock_ton_usdt(), Asset::from_chain(Chain::Ethereum), swap_data), + sender_address: "sender".to_string(), + destination_address: "0xrecipient".to_string(), + }; + assert_eq!(get_recipient_jetton_wallet(&transfer_swap), Some("ton_deposit_address")); + + let contract_swap = TransactionPreloadInput { + input_type: TransactionInputType::Swap( + Asset::mock_ton_usdt(), + Asset::from_chain(Chain::Ton), + SwapData::mock_contract(SwapProvider::StonfiV2, "2000000", "400000000", "300000000"), + ), + sender_address: "sender".to_string(), + destination_address: "recipient".to_string(), + }; + assert_eq!(get_recipient_jetton_wallet(&contract_swap), None); + } } diff --git a/crates/gem_ton/src/rpc/client.rs b/crates/gem_ton/src/rpc/client.rs index aeef232aa..4c502d92c 100644 --- a/crates/gem_ton/src/rpc/client.rs +++ b/crates/gem_ton/src/rpc/client.rs @@ -11,6 +11,7 @@ use crate::models::{ SimpleJettonBalance, WalletInfo, }; +#[derive(Debug)] pub struct TonClient { pub client: C, } diff --git a/crates/gem_ton/src/signer/transaction/sign.rs b/crates/gem_ton/src/signer/transaction/sign.rs index 48b5b45d0..c1ddd1bf4 100644 --- a/crates/gem_ton/src/signer/transaction/sign.rs +++ b/crates/gem_ton/src/signer/transaction/sign.rs @@ -175,11 +175,8 @@ mod tests { swap_data.data.value = "241000000".to_string(); swap_data.data.data = mock_cell(); swap_data.data.gas_limit = None; - let input = SignerInput::mock_with_input_type( + let input = SignerInput::mock_ton( TransactionInputType::Swap(Asset::from_chain(Chain::Ton), Asset::from_chain(Chain::Ton), swap_data), - "", - "", - "0", TransactionLoadMetadata::mock_ton(1), ); diff --git a/crates/primitives/src/testkit/asset_mock.rs b/crates/primitives/src/testkit/asset_mock.rs index 2708376a7..9a579789a 100644 --- a/crates/primitives/src/testkit/asset_mock.rs +++ b/crates/primitives/src/testkit/asset_mock.rs @@ -1,6 +1,6 @@ use crate::{ Asset, AssetId, AssetType, Chain, - asset_constants::{ETHEREUM_USDC_ASSET_ID, SOLANA_USDC_ASSET_ID}, + asset_constants::{ETHEREUM_USDC_ASSET_ID, SOLANA_USDC_ASSET_ID, TON_USDT_ASSET_ID}, }; impl Asset { @@ -20,6 +20,10 @@ impl Asset { Asset::new(ETHEREUM_USDC_ASSET_ID.clone(), "USD Coin".to_string(), "USDC".to_string(), 6, AssetType::ERC20) } + pub fn mock_ton_usdt() -> Self { + Asset::new(TON_USDT_ASSET_ID.clone(), "Tether USD".to_string(), "USDT".to_string(), 6, AssetType::JETTON) + } + pub fn mock_eth() -> Self { Asset::from_chain(Chain::Ethereum) } diff --git a/crates/primitives/src/testkit/swap_mock.rs b/crates/primitives/src/testkit/swap_mock.rs index 2c7be684d..de99e3f71 100644 --- a/crates/primitives/src/testkit/swap_mock.rs +++ b/crates/primitives/src/testkit/swap_mock.rs @@ -66,6 +66,25 @@ impl SwapData { data: SwapQuoteData::mock(), } } + + pub fn mock_contract(provider: SwapProvider, from_value: &str, to_value: &str, value: &str) -> Self { + let swap_data = Self::mock_with_values(provider, from_value, to_value); + SwapData { + data: SwapQuoteData { + value: value.to_string(), + ..swap_data.data + }, + ..swap_data + } + } + + pub fn mock_transfer(provider: SwapProvider, from_value: &str, to_value: &str, to: &str) -> Self { + let swap_data = Self::mock_with_values(provider, from_value, to_value); + SwapData { + data: SwapQuoteData::new_tranfer(to.to_string(), from_value.to_string(), None), + ..swap_data + } + } } impl SwapQuote { diff --git a/crates/primitives/src/testkit/transaction_load_input_mock.rs b/crates/primitives/src/testkit/transaction_load_input_mock.rs index d9be78439..b3386118c 100644 --- a/crates/primitives/src/testkit/transaction_load_input_mock.rs +++ b/crates/primitives/src/testkit/transaction_load_input_mock.rs @@ -97,6 +97,22 @@ impl SignerInput { TransactionFee::default(), ) } + + pub fn mock_ton(input_type: TransactionInputType, metadata: TransactionLoadMetadata) -> Self { + SignerInput::new( + TransactionLoadInput { + input_type, + sender_address: "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string(), + destination_address: "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg".to_string(), + value: "10000".to_string(), + gas_price: GasPriceType::regular(0), + memo: None, + is_max_value: false, + metadata, + }, + TransactionFee::default(), + ) + } } impl TransactionLoadInput { diff --git a/crates/swapper/src/chainflip/provider.rs b/crates/swapper/src/chainflip/provider.rs index b78d29460..360c85ca1 100644 --- a/crates/swapper/src/chainflip/provider.rs +++ b/crates/swapper/src/chainflip/provider.rs @@ -20,7 +20,7 @@ use crate::{ amount_to_value, approval::{check_approval_erc20, get_swap_gas_limit_with_approval}, cross_chain::VaultAddresses, - fees::{DEFAULT_CHAINFLIP_FEE_BPS, apply_slippage_in_bp, resolve_max_quote_value}, + fees::{DEFAULT_CHAINFLIP_FEE_BPS, apply_slippage_in_bp, quote_value_after_reserve_by_chain}, solana::DEFAULT_SWAP_GAS_LIMIT, }; use primitives::{ChainType, chain::Chain, swap::QuoteAsset}; @@ -70,7 +70,7 @@ where } fn get_quote_value(request: &QuoteRequest) -> Result { - let value = resolve_max_quote_value(request)?; + let value = quote_value_after_reserve_by_chain(request)?; if !request.options.use_max_amount || !request.from_asset.asset_id().is_native() { return Ok(value); } diff --git a/crates/swapper/src/fees/mod.rs b/crates/swapper/src/fees/mod.rs index 6e6ca378c..287980833 100644 --- a/crates/swapper/src/fees/mod.rs +++ b/crates/swapper/src/fees/mod.rs @@ -3,7 +3,7 @@ mod reserve; mod slippage; pub use referral::{ReferralFee, ReferralFees, default_referral_fees}; -pub use reserve::{RESERVED_NATIVE_FEES, reserved_tx_fees, resolve_max_quote_value}; +pub use reserve::{RESERVED_NATIVE_FEES, quote_value_after_reserve, quote_value_after_reserve_by_chain, reserved_tx_fees}; pub use slippage::{BasisPointConvert, apply_slippage_in_bp}; pub const DEFAULT_SWAP_FEE_BPS: u32 = 50; diff --git a/crates/swapper/src/fees/reserve.rs b/crates/swapper/src/fees/reserve.rs index dba49eab6..5dd8c6eb6 100644 --- a/crates/swapper/src/fees/reserve.rs +++ b/crates/swapper/src/fees/reserve.rs @@ -45,13 +45,10 @@ pub fn reserved_tx_fees(chain: Chain) -> Option<&'static str> { RESERVED_NATIVE_FEES.get(&chain).copied() } -pub fn resolve_max_quote_value(request: &QuoteRequest) -> Result { +pub fn quote_value_after_reserve(request: &QuoteRequest, reserved: &str) -> Result { if !request.options.use_max_amount || !request.from_asset.asset_id().is_native() { return Ok(request.value.clone()); } - let Some(reserved) = reserved_tx_fees(request.from_asset.chain()) else { - return Ok(request.value.clone()); - }; let reserved_fee = U256::from_str(reserved).map_err(|_| SwapperError::ComputeQuoteError(format!("invalid reserved fee: {reserved}")))?; let amount = U256::from_str(&request.value).map_err(|_| SwapperError::ComputeQuoteError(format!("invalid amount: {}", request.value)))?; if amount <= reserved_fee { @@ -61,3 +58,10 @@ pub fn resolve_max_quote_value(request: &QuoteRequest) -> Result Result { + let Some(reserved) = reserved_tx_fees(request.from_asset.chain()) else { + return Ok(request.value.clone()); + }; + quote_value_after_reserve(request, reserved) +} diff --git a/crates/swapper/src/near_intents/provider.rs b/crates/swapper/src/near_intents/provider.rs index 037c1469b..7e21e4a27 100644 --- a/crates/swapper/src/near_intents/provider.rs +++ b/crates/swapper/src/near_intents/provider.rs @@ -10,7 +10,7 @@ use crate::{ client_factory::create_client_with_chain, cross_chain::VaultAddresses, fees::DEFAULT_REFERRER, - fees::resolve_max_quote_value, + fees::quote_value_after_reserve_by_chain, near_intents::client::{base_url, explorer_url}, }; use async_trait::async_trait; @@ -292,7 +292,7 @@ where SwapperMode::ExactOut => return Err(SwapperError::NotSupportedAsset), }; - let amount = resolve_max_quote_value(request)?; + let amount = quote_value_after_reserve_by_chain(request)?; let quote_request = self.build_quote_request(request, mode, amount.clone(), true)?; let response = Self::extract_quote(self.client.fetch_quote("e_request).await?, request.from_asset.decimals)?; let amount_out = Self::parse_amount(&response.quote.amount_out, "amountOut")?; @@ -423,31 +423,31 @@ mod tests { } #[test] - fn resolve_quote_amount_with_use_max_reserves_fee() { + fn quote_amount_with_use_max_reserves_fee() { let reserve = U256::from_str(reserved_tx_fees(Chain::Ethereum).unwrap()).unwrap(); let amount = (reserve + U256::from(500u64)).to_string(); let request = build_quote_request(&amount, true, Chain::Ethereum); - let result = resolve_max_quote_value(&request).expect("expected amount to resolve"); + let result = quote_value_after_reserve_by_chain(&request).unwrap(); assert_eq!(result, (U256::from_str(&amount).unwrap() - reserve).to_string()); } #[test] - fn resolve_quote_amount_without_use_max_keeps_amount() { + fn quote_amount_without_use_max_keeps_amount() { let amount = "123456"; let request = build_quote_request(amount, false, Chain::Ethereum); - let result = resolve_max_quote_value(&request).expect("expected amount to resolve"); + let result = quote_value_after_reserve_by_chain(&request).unwrap(); assert_eq!(result, amount); } #[test] - fn resolve_quote_amount_rejects_when_under_reserved() { + fn quote_amount_rejects_when_under_reserved() { let reserve = U256::from_str(reserved_tx_fees(Chain::Ethereum).unwrap()).unwrap(); let request = build_quote_request(&reserve.to_string(), true, Chain::Ethereum); - let err = resolve_max_quote_value(&request).expect_err("expected error"); + let err = quote_value_after_reserve_by_chain(&request).expect_err("expected error"); assert!(matches!(err, SwapperError::InputAmountError { .. })); } @@ -537,7 +537,7 @@ mod tests { "message": "Amount is too low for bridge, try at least 8516130", }); - let decoded: QuoteResponseResult = serde_json::from_value(payload).expect("failed to decode error payload"); + let decoded: QuoteResponseResult = serde_json::from_value(payload).unwrap(); let QuoteResponseResult::Err(err) = decoded else { panic!("expected error variant"); diff --git a/crates/swapper/src/relay/provider.rs b/crates/swapper/src/relay/provider.rs index 8e059642f..c25f6011b 100644 --- a/crates/swapper/src/relay/provider.rs +++ b/crates/swapper/src/relay/provider.rs @@ -14,7 +14,7 @@ use super::{ }; use crate::{ FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, RpcClient, RpcProvider, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, - SwapperQuoteData, approval::check_approval_erc20, config::get_swap_api_url, cross_chain::VaultAddresses, fees::DEFAULT_REFERRER, fees::resolve_max_quote_value, + SwapperQuoteData, approval::check_approval_erc20, config::get_swap_api_url, cross_chain::VaultAddresses, fees::DEFAULT_REFERRER, fees::quote_value_after_reserve_by_chain, }; #[derive(Debug)] @@ -72,7 +72,7 @@ where let origin_currency = asset_to_currency(&from_asset_id)?; let destination_currency = asset_to_currency(&to_asset_id)?; let app_fees = resolve_app_fees(request); - let from_value = resolve_max_quote_value(request)?; + let from_value = quote_value_after_reserve_by_chain(request)?; let relay_request = RelayQuoteRequest { user: request.wallet_address.clone(), diff --git a/crates/swapper/src/squid/provider.rs b/crates/swapper/src/squid/provider.rs index 41e44f768..db8e1b1a3 100644 --- a/crates/swapper/src/squid/provider.rs +++ b/crates/swapper/src/squid/provider.rs @@ -11,7 +11,7 @@ use crate::{ SwapperQuoteData, config::{DEFAULT_SWAP_FEE_BPS, get_swap_api_url}, cross_chain::VaultAddresses, - fees::resolve_max_quote_value, + fees::quote_value_after_reserve_by_chain, }; #[derive(Debug)] @@ -112,7 +112,7 @@ where } async fn get_quote(&self, request: &QuoteRequest) -> Result { - let from_value = resolve_max_quote_value(request)?; + let from_value = quote_value_after_reserve_by_chain(request)?; let (response, _, _) = self.fetch_route(request, &from_value, true).await?; Ok(Quote { diff --git a/crates/swapper/src/stonfi/provider.rs b/crates/swapper/src/stonfi/provider.rs index 3086b4c2e..5301462e8 100644 --- a/crates/swapper/src/stonfi/provider.rs +++ b/crates/swapper/src/stonfi/provider.rs @@ -7,11 +7,11 @@ use crate::{ FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, RpcClient, RpcProvider, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteAsset, SwapperQuoteData, config::get_swap_api_url, - fees::{default_referral_fees, resolve_max_quote_value}, + fees::{default_referral_fees, quote_value_after_reserve_by_chain}, }; use async_trait::async_trait; use gem_client::Client; -use gem_ton::constants::TON_PROXY_JETTON_ADDRESS; +use gem_ton::{address::base64_to_hex_address, constants::TON_PROXY_JETTON_ADDRESS, rpc::client::TonClient}; use number_formatter::BigNumberFormatter; use primitives::Chain; use std::{fmt::Debug, sync::Arc}; @@ -25,11 +25,14 @@ where { provider: ProviderType, client: StonfiClient, + ton_client: TonClient, } impl Stonfi { pub fn new(rpc_provider: Arc) -> Self { - Self::new_with_client(RpcClient::new(get_swap_api_url("stonfi"), rpc_provider)) + let endpoint = rpc_provider.get_endpoint(Chain::Ton).expect("failed to get TON endpoint for STON.fi"); + let ton_client = TonClient::new(RpcClient::new(endpoint, rpc_provider.clone())); + Self::new_with_clients(RpcClient::new(get_swap_api_url("stonfi"), rpc_provider), ton_client) } } @@ -37,12 +40,31 @@ impl Stonfi where C: Client + Clone + Send + Sync + Debug + 'static, { - pub fn new_with_client(client: C) -> Self { + pub fn new_with_clients(client: C, ton_client: TonClient) -> Self { Self { provider: ProviderType::new(SwapperProvider::StonfiV2), client: StonfiClient::new(client), + ton_client, } } + + async fn sender_jetton_wallet(&self, quote: &Quote) -> Result, SwapperError> { + if quote.request.from_asset.is_native() { + return Ok(None); + } + let token_id = quote.request.from_asset.asset_id().token_id.ok_or(SwapperError::NotSupportedAsset)?; + let jetton_token_id = base64_to_hex_address(&token_id).ok_or(SwapperError::NotSupportedAsset)?.to_uppercase(); + let wallet = self + .ton_client + .get_jetton_wallets(quote.request.wallet_address.clone()) + .await + .map_err(|err| SwapperError::ComputeQuoteError(err.to_string()))? + .jetton_wallets + .into_iter() + .find(|wallet| wallet.jetton == jetton_token_id) + .ok_or_else(|| SwapperError::ComputeQuoteError("missing sender jetton wallet".into()))?; + Ok(Some(wallet.address)) + } } #[async_trait] @@ -59,7 +81,7 @@ where } async fn get_quote(&self, request: &QuoteRequest) -> Result { - let from_value = resolve_max_quote_value(request)?; + let from_value = quote_value_after_reserve_by_chain(request)?; let referral_fee = request.options.fee.clone().map(|fees| fees.ton).unwrap_or_else(|| default_referral_fees().ton); let simulation_request = SimulateSwapRequest { offer_address: token_address(&request.from_asset), @@ -98,11 +120,13 @@ where } else { "e.request.destination_address }; + let sender_jetton_wallet = self.sender_jetton_wallet(quote).await?; let tx = build_swap_transaction(SwapTransactionParams { simulation: &simulation, from_native: quote.request.from_asset.is_native(), to_native: quote.request.to_asset.is_native(), + sender_jetton_wallet: sender_jetton_wallet.as_deref(), from_value: "e.from_value, min_ask_amount: &simulation.min_ask_units, wallet_address: "e.request.wallet_address, diff --git a/crates/swapper/src/stonfi/tx_builder/model.rs b/crates/swapper/src/stonfi/tx_builder/model.rs index 7d2ecaadb..3b0f642fa 100644 --- a/crates/swapper/src/stonfi/tx_builder/model.rs +++ b/crates/swapper/src/stonfi/tx_builder/model.rs @@ -11,6 +11,7 @@ pub struct SwapTransactionParams<'a> { pub simulation: &'a SwapSimulation, pub from_native: bool, pub to_native: bool, + pub sender_jetton_wallet: Option<&'a str>, pub from_value: &'a str, pub min_ask_amount: &'a str, pub wallet_address: &'a str, diff --git a/crates/swapper/src/stonfi/tx_builder/tests.rs b/crates/swapper/src/stonfi/tx_builder/tests.rs index 6621fb23e..63906ebb8 100644 --- a/crates/swapper/src/stonfi/tx_builder/tests.rs +++ b/crates/swapper/src/stonfi/tx_builder/tests.rs @@ -2,6 +2,8 @@ use super::*; use crate::stonfi::{model::SwapSimulation, testkit::TEST_TON_WALLET_ADDRESS}; use gem_ton::tvm::BagOfCells; +const TEST_SENDER_JETTON_WALLET: &str = "EQAlgB03OjJKdXrlwZiGJD5snSzPKF2VL5bErJn_cqJANGH9"; + #[test] fn test_build_v1_swap_transaction() { let v1: SwapSimulation = serde_json::from_str(include_str!("../testdata/v1_simulation.json")).unwrap(); @@ -10,6 +12,7 @@ fn test_build_v1_swap_transaction() { simulation: &v1, from_native: true, to_native: false, + sender_jetton_wallet: None, from_value: "1000000000", min_ask_amount: &v1.min_ask_units, wallet_address: TEST_TON_WALLET_ADDRESS, @@ -39,6 +42,7 @@ fn test_build_v2_swap_transactions() { simulation: &v2, from_native: true, to_native: false, + sender_jetton_wallet: None, from_value: "1000000000", min_ask_amount: &v2.min_ask_units, wallet_address: TEST_TON_WALLET_ADDRESS, @@ -63,6 +67,7 @@ fn test_build_v2_swap_transactions() { simulation: &v2, from_native: false, to_native: true, + sender_jetton_wallet: Some(TEST_SENDER_JETTON_WALLET), from_value: "1000000", min_ask_amount: "740000000", wallet_address: TEST_TON_WALLET_ADDRESS, @@ -75,7 +80,7 @@ fn test_build_v2_swap_transactions() { }) .unwrap(); - assert_eq!(jetton_transaction.to, v2.offer_jetton_wallet); + assert_eq!(jetton_transaction.to, TEST_SENDER_JETTON_WALLET); assert_eq!(jetton_transaction.value, "300000000"); assert_eq!( jetton_transaction.data, diff --git a/crates/swapper/src/stonfi/tx_builder/v1.rs b/crates/swapper/src/stonfi/tx_builder/v1.rs index 51c8dfb50..0af037cb7 100644 --- a/crates/swapper/src/stonfi/tx_builder/v1.rs +++ b/crates/swapper/src/stonfi/tx_builder/v1.rs @@ -51,9 +51,12 @@ fn build_jetton_swap(params: SwapTransactionParams<'_>, swap_body: &CellArc) -> (V1_JETTON_TO_JETTON_GAS, V1_JETTON_TO_JETTON_FORWARD_GAS) }; let body = build_jetton_transfer_body(&from_value, &router, Some(&wallet), &BigUint::from(forward_gas), Some(swap_body))?; + let sender_jetton_wallet = params + .sender_jetton_wallet + .ok_or_else(|| SwapperError::ComputeQuoteError("missing sender jetton wallet".into()))?; Ok(TxParams { - to: params.simulation.offer_jetton_wallet.clone(), + to: sender_jetton_wallet.to_string(), value: gas.to_string(), data: BagOfCells::from_root(body).to_base64(true)?, }) diff --git a/crates/swapper/src/stonfi/tx_builder/v2.rs b/crates/swapper/src/stonfi/tx_builder/v2.rs index 9b5bb4e6c..8d4f8580f 100644 --- a/crates/swapper/src/stonfi/tx_builder/v2.rs +++ b/crates/swapper/src/stonfi/tx_builder/v2.rs @@ -49,9 +49,12 @@ fn build_jetton_swap(params: SwapTransactionParams<'_>, swap_body: &CellArc) -> let wallet = Address::parse(params.wallet_address)?; let from_value = BigUint::from_str(params.from_value)?; let body = build_jetton_transfer_body(&from_value, &router, Some(&wallet), &BigUint::from(V2_JETTON_SWAP_FORWARD_GAS), Some(swap_body))?; + let sender_jetton_wallet = params + .sender_jetton_wallet + .ok_or_else(|| SwapperError::ComputeQuoteError("missing sender jetton wallet".into()))?; Ok(TxParams { - to: params.simulation.offer_jetton_wallet.clone(), + to: sender_jetton_wallet.to_string(), value: V2_JETTON_SWAP_GAS.to_string(), data: BagOfCells::from_root(body).to_base64(true)?, }) diff --git a/skills/swapper-checklist.md b/skills/swapper-checklist.md index 0243bd50b..24b9e35e7 100644 --- a/skills/swapper-checklist.md +++ b/skills/swapper-checklist.md @@ -29,7 +29,7 @@ For the given provider, verify each item by reading the provider code and relate - [ ] Addresses match what the provider actually uses in transactions ### 6. Max Swap (use_max_amount) -- [ ] `get_quote()` calls `resolve_max_quote_value(request)?` from `crate::fees` +- [ ] `get_quote()` calls `quote_value_after_reserve_by_chain(request)?` from `crate::fees` - [ ] Adjusted value used for both the API quote request and `Quote.from_value` - [ ] Reserved fees for supported chains exist in `RESERVED_NATIVE_FEES` (`fees/reserve.rs`)