Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-02 15:36:13 +05:00
parent 613bb9226f
commit 3f1102f232
20 changed files with 247 additions and 172 deletions

View file

@ -21,7 +21,6 @@ import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.blockchain.network.ResultChecker
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -39,7 +38,6 @@ import java.math.RoundingMode
@Suppress("LargeClass")
class TransactionManagerImpl(
private val appStateHolder: AppStateHolder,
private val analytics: AnalyticsEventHandler,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
) : TransactionManager {
@ -144,10 +142,6 @@ class TransactionManagerImpl(
}
}
override fun getNativeTokenDecimals(networkId: String): Int {
return Blockchain.fromNetworkId(networkId)?.decimals() ?: error("blockchain not found")
}
override suspend fun updateWalletManager(networkId: String, derivationPath: String?) {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
getActualWalletManager(blockchain, derivationPath).update()
@ -205,7 +199,6 @@ class TransactionManagerImpl(
return ProxyNetworkInfo(
name = blockchain.fullName,
blockchainId = blockchain.id,
blockchainCurrency = blockchain.currency,
)
}

View file

@ -164,11 +164,6 @@ class UserWalletManagerImpl(
}
}
override fun getNetworkCurrency(networkId: String): String {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
return blockchain.currency
}
@Throws(IllegalArgumentException::class)
private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val selectedUserWallet = requireNotNull(

View file

@ -1,6 +1,5 @@
package com.tangem.tap.proxy.di
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -43,13 +42,11 @@ internal object ProxyModule {
@Singleton
fun provideTransactionManager(
appStateHolder: AppStateHolder,
analytics: AnalyticsEventHandler,
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
): TransactionManager {
return TransactionManagerImpl(
appStateHolder = appStateHolder,
analytics = analytics,
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
)

View file

@ -3,8 +3,8 @@ package com.tangem.domain.tokens.model
import java.math.BigDecimal
sealed class FeePaidCurrency {
object Coin : FeePaidCurrency()
object SameCurrency : FeePaidCurrency()
data object Coin : FeePaidCurrency()
data object SameCurrency : FeePaidCurrency()
data class Token(
val tokenId: CryptoCurrency.ID,
val name: String,

View file

@ -35,14 +35,14 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase(
return either {
when (feePaidCurrency) {
FeePaidCurrency.Coin -> getCryptoCurrency(operations, cryptoCurrency.id)
FeePaidCurrency.Coin -> getCryptoCurrencyStatus(operations, cryptoCurrency.id)
FeePaidCurrency.SameCurrency -> cryptoCurrencyStatus
is FeePaidCurrency.Token -> getCryptoCurrency(operations, feePaidCurrency.tokenId)
is FeePaidCurrency.Token -> getCryptoCurrencyStatus(operations, feePaidCurrency.tokenId)
}
}
}
private suspend fun getCryptoCurrency(
private suspend fun getCryptoCurrencyStatus(
operations: CurrenciesStatusesOperations,
cryptoCurrencyId: CryptoCurrency.ID,
): CryptoCurrencyStatus? {

View file

@ -3,5 +3,4 @@ package com.tangem.feature.swap.domain.models.domain
data class NetworkInfo(
val name: String,
val blockchainId: String,
val blockchainCurrency: String,
)

View file

@ -7,19 +7,18 @@ import com.tangem.feature.swap.domain.models.SwapAmount
*
* @property isAllowedToSpend shows is token allowed to spend
* @property isBalanceEnough shows is balance of token enough
* @property isFeeEnough shows is amount of main coin enough for fee
*/
// todo Refactor this state
data class PreparedSwapConfigState(
val isAllowedToSpend: Boolean,
val isBalanceEnough: Boolean,
val isFeeEnough: Boolean,
val feeState: SwapFeeState,
val hasOutgoingTransaction: Boolean,
val includeFeeInAmount: IncludeFeeInAmount,
)
sealed class IncludeFeeInAmount {
data class Included(val amountSubtractFee: SwapAmount) : IncludeFeeInAmount()
object Excluded : IncludeFeeInAmount()
object BalanceNotEnough : IncludeFeeInAmount()
data object Excluded : IncludeFeeInAmount()
data object BalanceNotEnough : IncludeFeeInAmount()
}

View file

@ -0,0 +1,12 @@
package com.tangem.feature.swap.domain.models.domain
import com.tangem.domain.tokens.model.CryptoCurrency
sealed class SwapFeeState {
data object Enough : SwapFeeState()
data class NotEnough(
val feeCurrency: CryptoCurrency? = null,
val currencyName: String? = null,
val currencySymbol: String? = null,
) : SwapFeeState()
}

View file

@ -3,10 +3,7 @@ package com.tangem.feature.swap.domain.models.ui
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.Warning
import com.tangem.feature.swap.domain.models.domain.*
import java.math.BigDecimal
sealed interface SwapState {
@ -15,11 +12,10 @@ sealed interface SwapState {
val fromTokenInfo: TokenSwapInfo,
val toTokenInfo: TokenSwapInfo,
val priceImpact: PriceImpact,
val networkCurrency: String,
val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = false,
isBalanceEnough = false,
isFeeEnough = false,
feeState = SwapFeeState.NotEnough(),
hasOutgoingTransaction = false,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
),
@ -98,7 +94,7 @@ sealed class TxFeeState {
val fee: TxFee,
) : TxFeeState()
object Empty : TxFeeState()
data object Empty : TxFeeState()
}
data class TxFee(

View file

@ -13,7 +13,6 @@ internal class DefaultBlockchainInteractor @Inject constructor(
NetworkInfo(
name = it.name,
blockchainId = it.blockchainId,
blockchainCurrency = it.blockchainCurrency,
)
}
}

View file

@ -13,7 +13,9 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.utils.convertToAmount
@ -61,6 +63,7 @@ internal class SwapInteractorImpl @Inject constructor(
private val swapTransactionRepository: SwapTransactionRepository,
private val currencyChecksRepository: CurrencyChecksRepository,
private val appCurrencyRepository: AppCurrencyRepository,
private val currenciesRepository: CurrenciesRepository,
private val initialToCurrencyResolver: InitialToCurrencyResolver,
) : SwapInteractor {
@ -251,7 +254,7 @@ internal class SwapInteractorImpl @Inject constructor(
if (amountDecimal == null || amountDecimal.signum() == 0) {
return providers.associateWith { createEmptyAmountState() }
}
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency))
val amount = SwapAmount(amountDecimal, fromToken.currency.decimals)
val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null)
val networkId = fromToken.currency.network.backendId
when (provider.type) {
@ -433,7 +436,7 @@ internal class SwapInteractorImpl @Inject constructor(
return when (swapProvider.type) {
ExchangeProviderType.CEX -> {
val amountDecimal = toBigDecimalOrNull(amountToSwap)
val amount = SwapAmount(requireNotNull(amountDecimal), getTokenDecimals(currencyToSend.currency))
val amount = SwapAmount(requireNotNull(amountDecimal), currencyToSend.currency.decimals)
val amountToSwapWithFee = if (includeFeeInAmount is IncludeFeeInAmount.Included) {
includeFeeInAmount.amountSubtractFee
} else {
@ -471,17 +474,28 @@ internal class SwapInteractorImpl @Inject constructor(
if (amountDecimal == null || amountDecimal.signum() == 0) {
return state
}
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency))
val amount = SwapAmount(amountDecimal, fromToken.currency.decimals)
val includeFeeInAmount = getIncludeFeeInAmount(
networkId = fromToken.currency.network.backendId,
txFee = state.txFee,
amount = amount,
fromToken = fromToken.currency,
)
val fee = when (val txFee = state.txFee) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
}
val feeState = getFeeState(
fee = fee,
spendAmount = amount,
networkId = fromToken.currency.network.backendId,
fromTokenStatus = fromToken,
)
return state.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = state.preparedSwapConfigState.copy(
isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
feeState = feeState,
isBalanceEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
includeFeeInAmount = includeFeeInAmount,
),
@ -497,7 +511,7 @@ internal class SwapInteractorImpl @Inject constructor(
fee: TxFee,
): TxState {
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
val amount = SwapAmount(amountDecimal, getTokenDecimals(currencyToSend))
val amount = SwapAmount(amountDecimal, currencyToSend.decimals)
val derivationPath = currencyToSend.network.derivationPath.value
val result = transactionManager.sendTransaction(
txData = SwapTxData(
@ -713,7 +727,7 @@ internal class SwapInteractorImpl @Inject constructor(
@Deprecated("used in old swap mechanism")
override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount {
return SwapAmount(token.value.amount ?: BigDecimal.ZERO, getTokenDecimals(token.currency))
return SwapAmount(token.value.amount ?: BigDecimal.ZERO, token.currency.decimals)
}
@Deprecated("used in old swap mechanism")
@ -734,14 +748,6 @@ internal class SwapInteractorImpl @Inject constructor(
return repository.getNativeTokenForNetwork(networkId)
}
private fun getTokenDecimals(token: CryptoCurrency): Int {
return if (token is CryptoCurrency.Token) {
token.decimals
} else {
transactionManager.getNativeTokenDecimals(token.network.backendId)
}
}
private suspend fun isAllowedToSpend(
networkId: String,
fromToken: CryptoCurrency,
@ -755,7 +761,7 @@ internal class SwapInteractorImpl @Inject constructor(
userWalletId = userWallet.walletId,
networkId = networkId,
derivationPath = fromToken.network.derivationPath.value,
tokenDecimalCount = getTokenDecimals(fromToken),
tokenDecimalCount = fromToken.decimals,
tokenAddress = getTokenAddress(fromToken),
spenderAddress = spenderAddress,
)
@ -797,7 +803,7 @@ internal class SwapInteractorImpl @Inject constructor(
val toToken = toTokenStatus.currency
return coroutineScope {
val txFee = if (provider.type == ExchangeProviderType.CEX) {
getFeeForCex(amount, fromTokenStatus, networkId)
getFeeForCex(amount, fromTokenStatus)
} else {
TxFeeState.Empty
}
@ -885,10 +891,21 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
ExchangeProviderType.CEX -> {
val fee = when (txFee) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
}
val feeState = getFeeState(
fee = fee,
spendAmount = amount,
networkId = networkId,
fromTokenStatus = fromToken,
)
swapState.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = PreparedSwapConfigState(
isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
feeState = feeState,
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
hasOutgoingTransaction = hasOutgoingTransaction(fromToken),
@ -911,26 +928,47 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
@Suppress("CyclomaticComplexMethod")
private suspend fun getIncludeFeeInAmount(
networkId: String,
txFee: TxFeeState,
amount: SwapAmount,
fromToken: CryptoCurrency,
): IncludeFeeInAmount {
val feeValue = when (txFee) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
}
val feePaidCurrency = getFeePaidCurrency(
userWalletId = requireNotNull(getSelectedWallet()).walletId,
currency = fromToken,
)
return when (feePaidCurrency) {
is FeePaidCurrency.Token -> {
if (feePaidCurrency.balance > feeValue) {
IncludeFeeInAmount.Excluded
} else {
IncludeFeeInAmount.BalanceNotEnough
}
}
else -> getIncludeFeeAmountForCoinFee(networkId, amount, feeValue, fromToken)
}
}
private suspend fun getIncludeFeeAmountForCoinFee(
networkId: String,
amount: SwapAmount,
feeValue: BigDecimal,
fromToken: CryptoCurrency,
): IncludeFeeInAmount {
val tokenForFeeBalance =
userWalletManager.getNativeTokenBalance(
networkId,
fromToken.network.derivationPath.value,
) ?: ProxyAmount.empty()
val feeValue = when (txFee) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
}
val amountWithFee = amount.value + feeValue
return when {
fromToken is CryptoCurrency.Token -> {
if (feeValue > tokenForFeeBalance.value || tokenForFeeBalance.value.signum() == 0) {
@ -950,7 +988,7 @@ internal class SwapInteractorImpl @Inject constructor(
IncludeFeeInAmount.Included(
SwapAmount(
tokenForFeeBalance.value - feeValue,
transactionManager.getNativeTokenDecimals(networkId),
getNativeToken(fromToken.network.backendId).decimals,
),
)
} else {
@ -960,11 +998,18 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun getFormattedFiatFees(networkId: String, vararg fees: BigDecimal): List<String> {
private suspend fun getFormattedFiatFees(fromToken: CryptoCurrency, vararg fees: BigDecimal): List<String> {
val appCurrency = getSelectedAppCurrencyUseCase.unwrap()
val nativeToken = repository.getNativeTokenForNetwork(networkId)
val rates = getQuotes(nativeToken.id)
return rates[nativeToken.id]?.fiatRate?.let { rate ->
val feePaidCurrency = getFeePaidCurrency(
userWalletId = requireNotNull(getSelectedWallet()).walletId,
currency = fromToken,
)
val feeCurrencyId: CryptoCurrency.ID = when (feePaidCurrency) {
is FeePaidCurrency.Token -> feePaidCurrency.tokenId
else -> getNativeToken(networkId = fromToken.network.backendId).id
}
val rates = getQuotes(feeCurrencyId)
return rates[feeCurrencyId]?.fiatRate?.let { rate ->
fees.map { fee ->
fee.toFiatString(rate, appCurrency.symbol, true)
}
@ -1006,16 +1051,16 @@ internal class SwapInteractorImpl @Inject constructor(
derivationPath = fromToken.currency.network.derivationPath.value,
)
val txFeeState = when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId)
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency)
}
val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState)
val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeByPriority)
val isFeeEnough = checkFeeIsEnough(
val feeState = getFeeState(
fee = feeByPriority,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken.currency,
fromTokenStatus = fromToken,
)
val swapState = updateBalances(
networkId = networkId,
@ -1032,7 +1077,7 @@ internal class SwapInteractorImpl @Inject constructor(
preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = true,
isBalanceEnough = isBalanceIncludeFeeEnough,
isFeeEnough = isFeeEnough,
feeState = feeState,
hasOutgoingTransaction = hasOutgoingTransaction(fromToken),
includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex
),
@ -1089,17 +1134,12 @@ internal class SwapInteractorImpl @Inject constructor(
toTokenAmount = toTokenAmount.value,
toRate = rates[toToken.id]?.fiatRate?.toDouble() ?: 0.0,
),
networkCurrency = userWalletManager.getNetworkCurrency(networkId),
swapDataModel = swapData,
txFee = txFeeState,
)
}
private suspend fun getFeeForCex(
amount: SwapAmount,
fromToken: CryptoCurrencyStatus,
networkId: String,
): TxFeeState {
private suspend fun getFeeForCex(amount: SwapAmount, fromToken: CryptoCurrencyStatus): TxFeeState {
getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId ->
val txFeeResult = estimateFeeUseCase(
amount = amount.value,
@ -1111,7 +1151,7 @@ internal class SwapInteractorImpl @Inject constructor(
TxFeeState.Empty
},
ifRight = { txFee ->
txFee.toTxFeeState(networkId)
txFee.toTxFeeState(fromToken.currency)
},
) ?: TxFeeState.Empty
}
@ -1170,8 +1210,8 @@ internal class SwapInteractorImpl @Inject constructor(
}
val feeState = feeData?.let {
when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId)
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken)
}
} ?: TxFeeState.Empty
val fee = when (feeState) {
@ -1179,11 +1219,11 @@ internal class SwapInteractorImpl @Inject constructor(
is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue
is TxFeeState.SingleFeeState -> feeState.fee.feeValue
}
val isFeeEnough = checkFeeIsEnough(
val swapFeeState = getFeeState(
fee = fee,
spendAmount = SwapAmount.zeroSwapAmount(),
networkId = networkId,
fromToken = fromToken,
fromTokenStatus = fromTokenStatus,
)
return quotesLoadedState.copy(
permissionState = PermissionDataState.PermissionReadyForRequest(
@ -1199,28 +1239,26 @@ internal class SwapInteractorImpl @Inject constructor(
),
),
preparedSwapConfigState = quotesLoadedState.preparedSwapConfigState.copy(
isFeeEnough = isFeeEnough,
feeState = swapFeeState,
),
)
}
private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(networkId: String): TxFeeState {
private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(fromToken: CryptoCurrency): TxFeeState {
val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee
val normalFeeGas = this.minFee.gasLimit.toInt()
val priorityFeeValue = this.normalFee.fee.value // in swap for priority use normal fee
val priorityFeeGas = this.normalFee.gasLimit.toInt()
val feesFiat = getFormattedFiatFees(networkId, normalFeeValue, priorityFeeValue)
val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue, priorityFeeValue)
val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" }
val priorityFiatFee = requireNotNull(feesFiat.getOrNull(1)) { "feesFiat item 1 couldn't be null" }
val networkCurrency = userWalletManager.getNetworkCurrency(networkId)
val decimals = transactionManager.getNativeTokenDecimals(networkId)
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeValue,
decimals = decimals,
decimals = minFee.fee.decimals,
)
val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = priorityFeeValue,
decimals = decimals,
decimals = normalFee.fee.decimals,
)
return TxFeeState.MultipleFeeState(
normalFee = TxFee(
@ -1228,8 +1266,8 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = normalFeeGas,
feeFiatFormatted = normalFiatFee,
feeCryptoFormatted = normalCryptoFee,
decimals = decimals,
cryptoSymbol = networkCurrency,
decimals = minFee.fee.decimals,
cryptoSymbol = minFee.fee.currencySymbol,
feeType = FeeType.NORMAL,
),
priorityFee = TxFee(
@ -1237,23 +1275,21 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = priorityFeeGas,
feeFiatFormatted = priorityFiatFee,
feeCryptoFormatted = priorityCryptoFee,
decimals = decimals,
cryptoSymbol = networkCurrency,
decimals = normalFee.fee.decimals,
cryptoSymbol = normalFee.fee.currencySymbol,
feeType = FeeType.PRIORITY,
),
)
}
private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState(networkId: String): TxFeeState {
private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState(fromToken: CryptoCurrency): TxFeeState {
val normalFeeValue = this.singleFee.fee.value
val normalFeeGas = this.singleFee.gasLimit.toInt()
val networkCurrency = userWalletManager.getNetworkCurrency(networkId)
val feesFiat = getFormattedFiatFees(networkId, normalFeeValue)
val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue)
val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" }
val decimals = transactionManager.getNativeTokenDecimals(networkId)
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeValue,
decimals = decimals,
decimals = singleFee.fee.decimals,
)
return TxFeeState.SingleFeeState(
fee = TxFee(
@ -1261,32 +1297,30 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = normalFeeGas,
feeFiatFormatted = normalFiatFee,
feeCryptoFormatted = normalCryptoFee,
decimals = decimals,
cryptoSymbol = networkCurrency,
decimals = singleFee.fee.decimals,
cryptoSymbol = singleFee.fee.currencySymbol,
feeType = FeeType.NORMAL,
),
)
}
private suspend fun TransactionFee.toTxFeeState(networkId: String): TxFeeState {
val networkCurrency = userWalletManager.getNetworkCurrency(networkId)
val decimals = transactionManager.getNativeTokenDecimals(networkId)
private suspend fun TransactionFee.toTxFeeState(fromToken: CryptoCurrency): TxFeeState {
return when (this) {
is TransactionFee.Choosable -> {
val normalFee = this.normal.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND)
val priorityFee = this.priority.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND)
val feeNormal = normalFee.amount.value ?: BigDecimal.ZERO
val feePriority = priorityFee.amount.value ?: BigDecimal.ZERO
val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0]
val priorityFiatValue = getFormattedFiatFees(networkId, feePriority)[0]
val normalFiatValue = getFormattedFiatFees(fromToken, feeNormal)[0]
val priorityFiatValue = getFormattedFiatFees(fromToken, feePriority)[0]
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeNormal,
decimals = decimals,
decimals = normalFee.amount.decimals,
)
val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feePriority,
decimals = decimals,
decimals = priorityFee.amount.decimals,
)
TxFeeState.MultipleFeeState(
normalFee = TxFee(
@ -1294,8 +1328,8 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = normalFee.getGasLimit(),
feeFiatFormatted = normalFiatValue,
feeCryptoFormatted = normalCryptoFee,
decimals = decimals,
cryptoSymbol = networkCurrency,
decimals = normalFee.amount.decimals,
cryptoSymbol = normalFee.amount.currencySymbol,
feeType = FeeType.NORMAL,
),
priorityFee = TxFee(
@ -1303,18 +1337,18 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = priorityFee.getGasLimit(),
feeFiatFormatted = priorityFiatValue,
feeCryptoFormatted = priorityCryptoFee,
decimals = decimals,
cryptoSymbol = networkCurrency,
decimals = priorityFee.amount.decimals,
cryptoSymbol = priorityFee.amount.currencySymbol,
feeType = FeeType.PRIORITY,
),
)
}
is TransactionFee.Single -> {
val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO
val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0]
val normalFiatValue = getFormattedFiatFees(fromToken, feeNormal)[0]
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeNormal,
decimals = transactionManager.getNativeTokenDecimals(networkId),
decimals = this.normal.amount.decimals,
)
TxFeeState.SingleFeeState(
fee = TxFee(
@ -1322,8 +1356,8 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = this.normal.getGasLimit(),
feeFiatFormatted = normalFiatValue,
feeCryptoFormatted = normalCryptoFee,
decimals = decimals,
cryptoSymbol = networkCurrency,
decimals = normal.amount.decimals,
cryptoSymbol = normal.amount.currencySymbol,
feeType = FeeType.NORMAL,
),
)
@ -1375,15 +1409,35 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private fun isBalanceEnough(fromToken: CryptoCurrencyStatus, amount: SwapAmount, fee: BigDecimal?): Boolean {
private suspend fun isBalanceEnough(
fromToken: CryptoCurrencyStatus,
amount: SwapAmount,
fee: BigDecimal?,
): Boolean {
val tokenBalance = getTokenBalance(fromToken).value
return if (fromToken.currency is CryptoCurrency.Token) {
tokenBalance >= amount.value
} else {
tokenBalance > amount.value.plus(fee ?: BigDecimal.ZERO)
val feePaidCurrency = getFeePaidCurrency(
userWalletId = requireNotNull(getSelectedWallet()).walletId,
currency = fromToken.currency,
)
return when (feePaidCurrency) {
is FeePaidCurrency.Token -> tokenBalance >= amount.value
else -> {
if (fromToken.currency is CryptoCurrency.Token) {
tokenBalance >= amount.value
} else {
tokenBalance > amount.value.plus(fee ?: BigDecimal.ZERO)
}
}
}
}
private suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
return currenciesRepository.getFeePaidCurrency(
userWalletId = userWalletId,
currency = currency,
)
}
private suspend fun getWalletAddress(networkId: String, derivationPath: String?): String {
return userWalletManager.getWalletAddress(networkId, derivationPath)
}
@ -1399,36 +1453,72 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun checkFeeIsEnough(
private fun toBigDecimalOrNull(amountToSwap: String): BigDecimal? {
return amountToSwap.replace(",", ".").toBigDecimalOrNull()
}
private suspend fun getFeeState(
fee: BigDecimal?,
spendAmount: SwapAmount,
networkId: String,
fromToken: CryptoCurrency,
): Boolean {
fromTokenStatus: CryptoCurrencyStatus,
): SwapFeeState {
val userWalletId = requireNotNull(getSelectedWallet()).walletId
if (fee == null) {
return false
return SwapFeeState.NotEnough()
}
val nativeTokenBalance = userWalletManager.getNativeTokenBalance(
networkId,
fromToken.network.derivationPath.value,
)
val percentsToFeeIncrease = BigDecimal.ONE
return when (fromToken) {
is CryptoCurrency.Coin -> {
nativeTokenBalance?.let { balance ->
return balance.value.minus(spendAmount.value) > fee.multiply(percentsToFeeIncrease)
} ?: false
}
is CryptoCurrency.Token -> {
nativeTokenBalance?.let { balance ->
return balance.value > fee.multiply(percentsToFeeIncrease)
} ?: false
}
}
}
private fun toBigDecimalOrNull(amountToSwap: String): BigDecimal? {
return amountToSwap.replace(",", ".").toBigDecimalOrNull()
val percentsToFeeIncrease = BigDecimal.ONE
return when (val feePaidCurrency = getFeePaidCurrency(userWalletId, fromTokenStatus.currency)) {
FeePaidCurrency.Coin -> {
val nativeTokenBalance = userWalletManager.getNativeTokenBalance(
networkId,
fromTokenStatus.currency.network.derivationPath.value,
)
nativeTokenBalance?.let { balance ->
if (balance.value.minus(spendAmount.value) > fee.multiply(percentsToFeeIncrease)) {
SwapFeeState.Enough
} else {
val nativeToken = getNativeToken(fromTokenStatus.currency.network.backendId)
SwapFeeState.NotEnough(
feeCurrency = nativeToken,
currencyName = nativeToken.network.name,
currencySymbol = nativeToken.symbol,
)
}
} ?: SwapFeeState.NotEnough()
}
FeePaidCurrency.SameCurrency -> {
val balance = fromTokenStatus.value.amount ?: return SwapFeeState.NotEnough()
if (balance.minus(spendAmount.value) > fee.multiply(percentsToFeeIncrease)) {
SwapFeeState.Enough
} else {
SwapFeeState.NotEnough(
feeCurrency = fromTokenStatus.currency,
currencyName = fromTokenStatus.currency.network.name,
currencySymbol = fromTokenStatus.currency.symbol,
)
}
}
is FeePaidCurrency.Token -> {
if (feePaidCurrency.balance > fee.multiply(percentsToFeeIncrease)) {
SwapFeeState.Enough
} else {
val token = currenciesRepository
.getMultiCurrencyWalletCurrenciesSync(userWalletId)
.find {
it is CryptoCurrency.Token &&
it.contractAddress.equals(feePaidCurrency.contractAddress, ignoreCase = true) &&
it.network.derivationPath == fromTokenStatus.currency.network.derivationPath
}
SwapFeeState.NotEnough(
feeCurrency = token,
currencyName = feePaidCurrency.name,
currencySymbol = feePaidCurrency.symbol,
)
}
}
}
}
private fun calculatePriceImpact(

View file

@ -47,6 +47,7 @@ class SwapDomainModule {
walletManagersFacade: WalletManagersFacade,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
initialToCurrencyResolver: InitialToCurrencyResolver,
currenciesRepository: CurrenciesRepository,
): SwapInteractor {
return SwapInteractorImpl(
transactionManager = transactionManager,
@ -62,6 +63,7 @@ class SwapDomainModule {
swapTransactionRepository = swapTransactionRepository,
appCurrencyRepository = appCurrencyRepository,
currencyChecksRepository = currencyChecksRepository,
currenciesRepository = currenciesRepository,
initialToCurrencyResolver = initialToCurrencyResolver,
)
}

View file

@ -15,7 +15,6 @@ import com.tangem.feature.swap.models.states.ProviderState
data class SwapStateHolder(
val sendCardData: SwapCardState,
val receiveCardData: SwapCardState,
val networkCurrency: String,
val blockchainId: String, // not the same as networkId, its local id in app
val warnings: List<SwapWarning> = emptyList(),
val alert: SwapWarning.GenericWarning? = null,

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.models
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.swap.domain.models.ui.TxFee
data class UiActions(
@ -20,7 +21,7 @@ data class UiActions(
val onSelectFeeType: (TxFee) -> Unit,
val onProviderClick: (String) -> Unit,
val onProviderSelect: (String) -> Unit,
val onBuyClick: () -> Unit,
val onBuyClick: (CryptoCurrency) -> Unit,
val onPolicyClick: (String) -> Unit,
val onTosClick: (String) -> Unit,
val onReceiveCardWarningClick: () -> Unit,

View file

@ -79,7 +79,6 @@ internal class StateBuilder(
isBalanceHidden = true,
),
fee = FeeItemState.Empty,
networkCurrency = networkInfo.blockchainCurrency,
swapButton = SwapButton(enabled = false, onClick = {}),
onRefresh = {},
onBackClicked = actions.onBackClicked,
@ -250,7 +249,6 @@ internal class StateBuilder(
balance = toCurrencyStatus.getFormattedAmount(isNeedSymbol = false),
isBalanceHidden = isBalanceHiddenProvider(),
),
networkCurrency = quoteModel.networkCurrency,
warnings = warnings,
permissionState = convertPermissionState(
lastPermissionState = uiStateHolder.permissionState,
@ -317,7 +315,7 @@ internal class StateBuilder(
val warnings = mutableListOf<SwapWarning>()
addDomainWarnings(quoteModel, warnings)
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough &&
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
) {
warnings.add(
@ -412,7 +410,8 @@ internal class StateBuilder(
fromToken: CryptoCurrency,
warnings: MutableList<SwapWarning>,
) {
if (!quoteModel.preparedSwapConfigState.isFeeEnough &&
val feeEnoughState = quoteModel.preparedSwapConfigState.feeState
if (feeEnoughState is SwapFeeState.NotEnough &&
quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.permissionState !is PermissionDataState.PermissionLoading
) {
@ -420,7 +419,9 @@ internal class StateBuilder(
SwapWarning.UnableToCoverFeeWarning(
createUnableToCoverFeeNotificationConfig(
fromToken = fromToken,
onBuyClick = actions.onBuyClick,
feeCurrency = feeEnoughState.feeCurrency,
currencyName = feeEnoughState.currencyName ?: fromToken.network.name,
currencySymbol = feeEnoughState.currencySymbol ?: fromToken.network.currencySymbol,
),
),
)
@ -438,7 +439,7 @@ internal class StateBuilder(
IncludeFeeInAmount.Excluded ->
preparedSwapConfigState.isAllowedToSpend &&
preparedSwapConfigState.isBalanceEnough &&
preparedSwapConfigState.isFeeEnough
preparedSwapConfigState.feeState is SwapFeeState.Enough
is IncludeFeeInAmount.Included -> true
}
}
@ -1219,8 +1220,16 @@ internal class StateBuilder(
private fun createUnableToCoverFeeNotificationConfig(
fromToken: CryptoCurrency,
onBuyClick: () -> Unit,
feeCurrency: CryptoCurrency?,
currencyName: String,
currencySymbol: String,
): NotificationConfig {
val buttonState = feeCurrency?.let {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_buy_currency, wrappedList(currencySymbol)),
onClick = { actions.onBuyClick(it) },
)
}
return NotificationConfig(
title = resourceReference(
R.string.warning_express_not_enough_fee_for_token_tx_title,
@ -1228,13 +1237,10 @@ internal class StateBuilder(
),
subtitle = resourceReference(
R.string.warning_express_not_enough_fee_for_token_tx_description,
wrappedList(fromToken.network.name, fromToken.network.currencySymbol),
wrappedList(currencyName, currencySymbol),
),
iconResId = fromToken.networkIconResId,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_buy_currency, wrappedList(fromToken.network.currencySymbol)),
onClick = onBuyClick,
),
buttonsState = buttonState,
)
}

View file

@ -490,7 +490,6 @@ private val state = SwapStateHolder(
),
),
),
networkCurrency = "MATIC",
swapButton = SwapButton(enabled = true, onClick = {}),
onRefresh = {},
onBackClicked = {},

View file

@ -932,12 +932,11 @@ internal class SwapViewModel @Inject constructor(
)
}
},
onBuyClick = {
swapInteractor.getSelectedWallet()?.let {
val fromToken = dataState.fromCryptoCurrency ?: return@let
onBuyClick = { currency ->
swapInteractor.getSelectedWallet()?.let { userWallet ->
swapRouter.openTokenDetails(
it.walletId,
swapInteractor.getNativeToken(fromToken.currency.network.backendId),
userWalletId = userWallet.walletId,
currency = currency,
)
}
},

View file

@ -54,9 +54,6 @@ interface TransactionManager {
derivationPath: String?,
): ProxyFees
@Throws(IllegalStateException::class)
fun getNativeTokenDecimals(networkId: String): Int
@Throws(IllegalStateException::class)
suspend fun updateWalletManager(networkId: String, derivationPath: String?)

View file

@ -59,13 +59,6 @@ interface UserWalletManager {
@Throws(IllegalStateException::class)
suspend fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount?
/**
* @param networkId
* @return currency name
*/
@Throws(IllegalStateException::class)
fun getNetworkCurrency(networkId: String): String
@Throws(IllegalStateException::class)
suspend fun getLastTransactionHash(networkId: String, derivationPath: String?): String?
}

View file

@ -3,5 +3,4 @@ package com.tangem.lib.crypto.models
data class ProxyNetworkInfo(
val name: String,
val blockchainId: String,
val blockchainCurrency: String,
)