Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c477e96
add support for trade fee estimation specific for TPU
Sep 20, 2025
0c064fe
fix fmt
Sep 20, 2025
87df052
fix wasm clippy
Sep 20, 2025
f5314e0
rename to check_balance_for_swap used for both maker and taker
Sep 20, 2025
20a3684
fix check and fmt
Sep 20, 2025
efc9643
add doc comment to new trait
Sep 20, 2025
fd43de6
fix paid_from_trading_vol in fee_helper
Sep 20, 2025
0fb360f
fix legacy taker swap preimage value in SwapTotalFeeHelper
Sep 20, 2025
303c02c
fix clippy
Sep 20, 2025
b638da7
fix base/rel in swap fee_helper
Sep 21, 2025
9513848
fix(fee_helper): proper trade amount, proper check for plaform coin f…
Sep 21, 2025
ad0202d
fix paid_from_trading_vol in updated check_balance_for_swap
Sep 21, 2025
4895433
use gas_limit const to get taker funding fee
Sep 21, 2025
e51838d
fix(fee_helper): handle DexFee::NoFee
Sep 21, 2025
3425781
fix clippy
Sep 21, 2025
17a7747
use fee_helper in taker_swap_v2 start
Sep 21, 2025
c49542f
fix(fee_helper): use TPU fees with check_other_coin_balance_for_swap …
Sep 22, 2025
f94eb60
fix(tests, fee_helper): update test expected for refund fee
Sep 22, 2025
61c7011
fix(tests): more expected amount fixes
Sep 22, 2025
c3654d8
fix(tests): another expected amount fix
Sep 22, 2025
a22a9c4
use fee_helper (supporting legacy and TPU) to calc max vol
Sep 23, 2025
efcd482
revert debug colors
Sep 23, 2025
812c8fc
fix is_platform_coin cond
Sep 23, 2025
c7ba804
add stage to TPU fee estimate fn's
Sep 23, 2025
ab69f7b
fix(tests): ignore failing coins tests
Sep 23, 2025
d6b88a3
revert log colors
Sep 23, 2025
b084c29
fix(tests): add timeout in qrc20 test
Sep 24, 2025
446b229
fix update_maker_order broken in fee_helper refactoring
Sep 24, 2025
0bec3c7
remove extra debug log
Sep 24, 2025
c3cb764
ignore 3 failing tests
Sep 24, 2025
3c9a4ce
add max vol tests for tpu
Sep 25, 2025
12d97a5
subtract min_tx_amount in max taker vol
Sep 26, 2025
baf6626
add not sufficient error to early report if required negative
Sep 26, 2025
b17d457
more test balances fixes due to dust added to max taker vol
Sep 26, 2025
f0e99ea
fix(test): fix calc dexfee in qtum test
Sep 26, 2025
2db077b
fix(test): more qtum test calc fix
Sep 26, 2025
1c746bd
fix(tpu): add dust for max taker vol only for TPU
Sep 26, 2025
75908bb
fix(tests): revert fixes for legacy max vol expected amounts
Sep 26, 2025
44c3e34
remove debug logging
Sep 26, 2025
d703636
add test_get_max_maker_vol_v2 test for tpu
Sep 26, 2025
632674f
add doc comment about dust
Sep 28, 2025
3ab7dc0
Merge branch 'dev' into refactor-TPU-fee-calc
Oct 9, 2025
e4a014d
fix clippy wasm
Oct 9, 2025
68d1fd0
fix(trade_fee): use gas limit const for TPU maker payment (instead of…
Oct 9, 2025
393a4e6
add note about test failure reason
Oct 10, 2025
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
159 changes: 114 additions & 45 deletions mm2src/coins/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ impl EthGasLimitV2 {
coin_type: &EthCoinType,
payment_type: EthPaymentType,
method: PaymentMethod,
) -> Result<u64, String> {
) -> Result<U256, String> {
match coin_type {
EthCoinType::Eth => {
let gas_limit = match payment_type {
Expand All @@ -451,7 +451,7 @@ impl EthGasLimitV2 {
PaymentMethod::RefundSecret => self.taker.eth_taker_refund_secret,
},
};
Ok(gas_limit)
Ok(gas_limit.into())
},
EthCoinType::Erc20 { .. } => {
let gas_limit = match payment_type {
Expand All @@ -468,25 +468,25 @@ impl EthGasLimitV2 {
PaymentMethod::RefundSecret => self.taker.erc20_taker_refund_secret,
},
};
Ok(gas_limit)
Ok(gas_limit.into())
},
EthCoinType::Nft { .. } => Err("NFT protocol is not supported for ETH and ERC20 Swaps".to_string()),
}
}

fn nft_gas_limit(&self, contract_type: &ContractType, method: PaymentMethod) -> u64 {
fn nft_gas_limit(&self, contract_type: &ContractType, method: PaymentMethod) -> U256 {
match contract_type {
ContractType::Erc1155 => match method {
PaymentMethod::Send => self.nft_maker.erc1155_payment,
PaymentMethod::Spend => self.nft_maker.erc1155_taker_spend,
PaymentMethod::RefundTimelock => self.nft_maker.erc1155_maker_refund_timelock,
PaymentMethod::RefundSecret => self.nft_maker.erc1155_maker_refund_secret,
PaymentMethod::Send => self.nft_maker.erc1155_payment.into(),
PaymentMethod::Spend => self.nft_maker.erc1155_taker_spend.into(),
PaymentMethod::RefundTimelock => self.nft_maker.erc1155_maker_refund_timelock.into(),
PaymentMethod::RefundSecret => self.nft_maker.erc1155_maker_refund_secret.into(),
},
ContractType::Erc721 => match method {
PaymentMethod::Send => self.nft_maker.erc721_payment,
PaymentMethod::Spend => self.nft_maker.erc721_taker_spend,
PaymentMethod::RefundTimelock => self.nft_maker.erc721_maker_refund_timelock,
PaymentMethod::RefundSecret => self.nft_maker.erc721_maker_refund_secret,
PaymentMethod::Send => self.nft_maker.erc721_payment.into(),
PaymentMethod::Spend => self.nft_maker.erc721_taker_spend.into(),
PaymentMethod::RefundTimelock => self.nft_maker.erc721_maker_refund_timelock.into(),
PaymentMethod::RefundSecret => self.nft_maker.erc721_maker_refund_secret.into(),
},
}
}
Expand Down Expand Up @@ -3960,6 +3960,32 @@ impl EthCoin {
async fn get_address_lock(&self, address: Address) -> Arc<AsyncMutex<()>> {
self.address_nonce_locks.get_adddress_lock(address).await
}

/// Estimate gas for the token `approve` contract call
async fn estimate_approve_gas_limit(
&self,
token_addr: Address,
value: TradePreimageValue,
) -> TradePreimageResult<U256> {
let value = match value {
TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => {
u256_from_big_decimal(&value, self.decimals).map_mm_err()?
},
};
let allowed = self.allowance(self.swap_contract_address).compat().await.map_mm_err()?;
if allowed < value {
// Pass a dummy spender. Let's use `my_address`.
let spender = self.derivation_method.single_addr_or_err().await.map_mm_err()?;
let approve_function = ERC20_CONTRACT.function("approve")?;
let approve_data = approve_function.encode_input(&[Token::Address(spender), Token::Uint(value)])?;
let (approve_gas_limit, _) = self
.estimate_gas_for_contract_call(token_addr, Bytes::from(approve_data), 0.into())
.await
.map_mm_err()?;
return Ok(approve_gas_limit);
}
Ok(0.into())
}
}

#[cfg_attr(test, mockable)]
Expand All @@ -3982,10 +4008,12 @@ impl EthCoin {
default_gas
} else {
match &action {
Action::Call(contract_addr) => coin
.estimate_gas_for_contract_call_if_conf(*contract_addr, Bytes::from(data.clone()), value)
.await
.map_err(|err| TransactionErr::Plain(ERRL!("{}", err.get_inner())))?,
Action::Call(contract_addr) => {
coin.estimate_gas_for_contract_call_if_conf(*contract_addr, Bytes::from(data.clone()), value)
.await
.map_err(|err| TransactionErr::Plain(ERRL!("{}", err.get_inner())))?
.0
},
_ => return Err(TransactionErr::InternalError("no gas limit set".to_owned())),
}
};
Expand Down Expand Up @@ -4888,7 +4916,7 @@ impl EthCoin {
contract_addr: Address,
call_data: Bytes,
value: U256,
) -> Web3RpcResult<U256> {
) -> Web3RpcResult<(U256, U256)> {
let coin = self.clone();
let my_address = coin.derivation_method.single_addr_or_err().await.map_mm_err()?;
let fee_policy_for_estimate =
Expand All @@ -4906,11 +4934,17 @@ impl EthCoin {
};
// gas price must be supplied because some smart contracts base their
// logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643
let estimate_gas_req = call_request_with_pay_for_gas_option(estimate_gas_req, pay_for_gas_option);
coin.estimate_gas_wrapper(estimate_gas_req)
let estimate_gas_req = call_request_with_pay_for_gas_option(estimate_gas_req, pay_for_gas_option.clone());
let gas_limit = coin
.estimate_gas_wrapper(estimate_gas_req)
.compat()
.await
.map_to_mm(Web3RpcError::from)
.map_to_mm(Web3RpcError::from)?;
let fee_per_gas = match pay_for_gas_option {
PayForGasOption::Legacy { gas_price } => gas_price,
PayForGasOption::Eip1559 { max_fee_per_gas, .. } => max_fee_per_gas,
};
Ok((gas_limit, fee_per_gas))
}

/// Calls estimate_gas_for_contract_call if the `estimate_gas_mult` conf param is set or `default_gas` is None.
Expand All @@ -4920,16 +4954,16 @@ impl EthCoin {
contract_addr: Address,
call_data: Bytes,
value: U256,
) -> Web3RpcResult<U256> {
let gas_estimated = self
) -> Web3RpcResult<(U256, U256)> {
let (gas_estimated, fee_per_gas) = self
.estimate_gas_for_contract_call(contract_addr, call_data, value)
.await?;
if let Some(estimate_gas_mult) = self.estimate_gas_mult {
let gas_estimated = u256_to_big_decimal(gas_estimated, 0).map_mm_err()?
* BigDecimal::from_f64(estimate_gas_mult).unwrap_or(BigDecimal::from(1));
Ok(u256_from_big_decimal(&gas_estimated, 0).map_mm_err()?)
Ok((u256_from_big_decimal(&gas_estimated, 0).map_mm_err()?, fee_per_gas))
} else {
Ok(gas_estimated)
Ok((gas_estimated, fee_per_gas))
}
}

Expand Down Expand Up @@ -5902,6 +5936,27 @@ impl EthCoin {
},
}
}

/// Estimated trade fee for the provided gas limit
pub async fn estimate_trade_fee(&self, gas_limit: U256, stage: FeeApproxStage) -> TradePreimageResult<TradeFee> {
let pay_for_gas_option = self
.get_swap_pay_for_gas_option(self.get_swap_gas_fee_policy().await.map_mm_err()?)
.await
.map_mm_err()?;
let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage);
let total_fee = calc_total_fee(gas_limit, &pay_for_gas_option).map_mm_err()?;
let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS).map_mm_err()?;
let fee_coin = match &self.coin_type {
EthCoinType::Eth => &self.ticker,
EthCoinType::Erc20 { platform, .. } => platform,
EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported),
};
Ok(TradeFee {
coin: fee_coin.into(),
amount: amount.into(),
paid_from_trading_vol: false,
})
}
}

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
Expand Down Expand Up @@ -6077,7 +6132,6 @@ impl MmCoin for EthCoin {
let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage);
let gas_limit = match self.coin_type {
EthCoinType::Eth => {
//let eth_payment_gas = self.
// this gas_limit includes gas for `ethPayment` and optionally `senderRefund` contract calls
if matches!(stage, FeeApproxStage::OrderIssueMax | FeeApproxStage::TradePreimageMax) {
U256::from(self.gas_limit.eth_payment) + U256::from(self.gas_limit.eth_sender_refund)
Expand All @@ -6087,27 +6141,9 @@ impl MmCoin for EthCoin {
},
EthCoinType::Erc20 { token_addr, .. } => {
let mut gas = U256::from(self.gas_limit.erc20_payment);
let value = match value {
TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => {
u256_from_big_decimal(&value, self.decimals).map_mm_err()?
},
};
let allowed = self.allowance(self.swap_contract_address).compat().await.map_mm_err()?;
if allowed < value {
// estimate gas for the `approve` contract call

// Pass a dummy spender. Let's use `my_address`.
let spender = self.derivation_method.single_addr_or_err().await.map_mm_err()?;
let approve_function = ERC20_CONTRACT.function("approve")?;
let approve_data = approve_function.encode_input(&[Token::Address(spender), Token::Uint(value)])?;
let approve_gas_limit = self
.estimate_gas_for_contract_call(token_addr, Bytes::from(approve_data), 0.into())
.await
.map_mm_err()?;
// Add gas for `approve` `erc20Payment` contract calls or zero
gas += self.estimate_approve_gas_limit(token_addr, value).await?;

// this gas_limit includes gas for `approve`, `erc20Payment` contract calls
gas += approve_gas_limit;
}
// add 'senderRefund' gas if requested
if matches!(stage, FeeApproxStage::TradePreimage | FeeApproxStage::TradePreimageMax) {
gas += U256::from(self.gas_limit.erc20_sender_refund);
Expand Down Expand Up @@ -7551,6 +7587,24 @@ impl Eip1559Ops for EthCoin {

#[async_trait]
impl TakerCoinSwapOpsV2 for EthCoin {
/// Wrapper for [EthCoin::get_fee_to_send_taker_funding_impl]
async fn get_fee_to_send_taker_funding(&self, args: GetTakerFundingFeeArgs) -> TradePreimageResult<TradeFee> {
self.get_fee_to_send_taker_funding_impl(args).await
}

async fn get_fee_to_spend_taker_funding(&self, stage: FeeApproxStage) -> TradePreimageResult<TradeFee> {
self.get_fee_to_spend_taker_funding_impl(stage).await
}

async fn get_fee_to_spend_taker_payment(&self, stage: FeeApproxStage) -> TradePreimageResult<TradeFee> {
self.get_fee_to_spend_taker_payment_impl(stage).await
}

/// Estimate tx fee to spend taker payment
async fn get_fee_to_refund_taker_payment(&self, stage: FeeApproxStage) -> TradePreimageResult<TradeFee> {
self.get_fee_to_refund_taker_payment_impl(stage).await
}

/// Wrapper for [EthCoin::send_taker_funding_impl]
async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result<Self::Tx, TransactionErr> {
self.send_taker_funding_impl(args).await
Expand Down Expand Up @@ -7759,6 +7813,21 @@ impl EthCoin {

#[async_trait]
impl MakerCoinSwapOpsV2 for EthCoin {
async fn get_fee_to_send_maker_payment_v2(
&self,
args: GetFeeToSendMakerPaymentArgs,
) -> TradePreimageResult<TradeFee> {
self.get_fee_to_send_maker_payment_v2_impl(args).await
}

async fn get_fee_to_spend_maker_payment_v2(&self, stage: FeeApproxStage) -> TradePreimageResult<TradeFee> {
self.get_fee_to_spend_maker_payment_v2_impl(stage).await
}

async fn get_fee_to_refund_maker_payment_v2(&self, stage: FeeApproxStage) -> TradePreimageResult<TradeFee> {
self.get_fee_to_refund_maker_payment_v2_impl(stage).await
}

async fn send_maker_payment_v2(&self, args: SendMakerPaymentArgs<'_, Self>) -> Result<Self::Tx, TransactionErr> {
self.send_maker_payment_v2_impl(args).await
}
Expand Down
58 changes: 51 additions & 7 deletions mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ use super::{
};
use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult};
use crate::eth::{
decode_contract_call, get_function_input_data, u256_from_big_decimal, EthCoin, EthCoinType, SignedEthTx,
MAKER_SWAP_V2,
decode_contract_call, get_function_input_data, u256_from_big_decimal, EthCoin, EthCoinType, FeeApproxStage,
SignedEthTx, MAKER_SWAP_V2,
};
use crate::{
ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, SendMakerPaymentArgs,
SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, TransactionErr, ValidateMakerPaymentArgs,
GetFeeToSendMakerPaymentArgs, ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs,
SendMakerPaymentArgs, SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, TradeFee, TradePreimageError,
TradePreimageResult, TransactionErr, ValidateMakerPaymentArgs,
};
use ethabi::{Function, Token};
use ethcore_transaction::Action;
Expand Down Expand Up @@ -57,6 +58,24 @@ struct MakerRefundSecretArgs<'a> {
}

impl EthCoin {
pub(crate) async fn get_fee_to_send_maker_payment_v2_impl(
&self,
args: GetFeeToSendMakerPaymentArgs,
) -> TradePreimageResult<TradeFee> {
let mut gas_limit = self
.gas_limit_v2
.gas_limit(&self.coin_type, EthPaymentType::MakerPayments, PaymentMethod::Send)
.map_err(|e| TradePreimageError::InternalError(ERRL!("{}", e)))?;

// Add token approval call gas limit
// TODO: for taker funding we have the taker.approve_payment constant.
// Maybe we could make the same for maker and save extra call
if let EthCoinType::Erc20 { token_addr, .. } = self.coin_type {
gas_limit += self.estimate_approve_gas_limit(token_addr, args.amount).await?;
}
self.estimate_trade_fee(gas_limit, args.stage).await
}

pub(crate) async fn send_maker_payment_v2_impl(
&self,
args: SendMakerPaymentArgs<'_, Self>,
Expand Down Expand Up @@ -222,7 +241,7 @@ impl EthCoin {
U256::from(ZERO_VALUE),
Action::Call(maker_swap_v2_contract),
data,
Some(U256::from(gas_limit)),
Some(gas_limit),
)
.compat()
.await
Expand Down Expand Up @@ -267,12 +286,37 @@ impl EthCoin {
U256::from(ZERO_VALUE),
Action::Call(maker_swap_v2_contract),
data,
Some(U256::from(gas_limit)),
Some(gas_limit),
)
.compat()
.await
}

/// Estimate spend payment fee using the gas_limit const.
/// Using the gas_limit const because we cannot get correct estimation from the chain for spending a non-existent payment.
pub(crate) async fn get_fee_to_spend_maker_payment_v2_impl(
&self,
stage: FeeApproxStage,
) -> TradePreimageResult<TradeFee> {
let gas_limit = self
.gas_limit_v2
.gas_limit(&self.coin_type, EthPaymentType::MakerPayments, PaymentMethod::Spend)
.map_err(|e| TradePreimageError::InternalError(ERRL!("{}", e)))?;
self.estimate_trade_fee(gas_limit, stage).await
}

/// Estimate refund payment fee
pub(crate) async fn get_fee_to_refund_maker_payment_v2_impl(
&self,
stage: FeeApproxStage,
) -> TradePreimageResult<TradeFee> {
let gas_limit = self
.gas_limit_v2
.gas_limit(&self.coin_type, EthPaymentType::MakerPayments, PaymentMethod::Spend)
.map_err(|e| TradePreimageError::InternalError(ERRL!("{}", e)))?;
self.estimate_trade_fee(gas_limit, stage).await
}

pub(crate) async fn spend_maker_payment_v2_impl(
&self,
args: SpendMakerPaymentArgs<'_, Self>,
Expand All @@ -295,7 +339,7 @@ impl EthCoin {
U256::from(ZERO_VALUE),
Action::Call(maker_swap_v2_contract),
data,
Some(U256::from(gas_limit)),
Some(gas_limit),
)
.compat()
.await
Expand Down
Loading
Loading