diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt index e5edb01fa4..3dcbf37cb7 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt @@ -92,7 +92,6 @@ data class WalletData( ) walletWarnings.add(warning) } - } private fun assembleTokenWarnings(walletWarnings: MutableList) = with(currency) { diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 199838f94c..e0671b7ffd 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -35,8 +35,24 @@ internal class SwapRepositoryImpl @Inject constructor( private val approveConverter = ApproveConverter() override suspend fun getRates(currencyId: String, tokenIds: List): Map { + // workaround cause backend do not return arbitrum and optimism rates + val addedTokens = if (tokenIds.contains(OPTIMISM_ID) || tokenIds.contains(ARBITRUM_ID)) { + tokenIds.toMutableList().apply { + add(ETHEREUM_ID) + } + } else { + tokenIds + } return withContext(coroutineDispatcher.io) { - tangemTechApi.getRates(currencyId.lowercase(), tokenIds.joinToString(",")).rates + val rates = tangemTechApi.getRates(currencyId.lowercase(), addedTokens.joinToString(",")).rates + val ethRate = rates[ETHEREUM_ID] + rates.mapValues { + if (it.key == OPTIMISM_ID || it.key == ARBITRUM_ID) { + ethRate ?: 0.0 + } else { + it.value + } + } } } @@ -136,4 +152,11 @@ internal class SwapRepositoryImpl @Inject constructor( private fun getOneInchApi(networkId: String): OneInchApi { return oneInchApiFactory.getApi(networkId) } + + companion object { + // TODO("get this ids from blockchain enum later") + private const val OPTIMISM_ID = "optimistic-ethereum" + private const val ARBITRUM_ID = "arbitrum-one" + private const val ETHEREUM_ID = "ethereum" + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 23b6c794b8..3b78f398f7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -120,7 +120,7 @@ internal class SwapInteractorImpl @Inject constructor( val result = transactionManager.sendApproveTransaction( networkId = networkId, feeAmount = fee, - estimatedGas = increasedEstimatedGas, + estimatedGas = estimatedGas, destinationAddress = transactionData.toAddress, dataToSign = transactionData.data, ) @@ -152,38 +152,54 @@ internal class SwapInteractorImpl @Inject constructor( val fromTokenAddress = getTokenAddress(fromToken) val toTokenAddress = getTokenAddress(toToken) val isAllowedToSpend = checkAllowance(networkId, fromTokenAddress) - val fee = getAndUpdateFee(networkId, fromToken) - val isBalanceEnough = isBalanceEnough(fromToken, amount, fee) - val isFeeEnough = checkFeeIsEnough(fee, amount, networkId, fromToken) if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) transactionManager.updateWalletManager(networkId) } + // load initial quotes data, it works despite balance + val quotesData = loadQuoteData( + networkId = networkId, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + amount = amount, + fromToken = fromToken, + toToken = toToken, + ) + // get fee from loaded quotes data, if error, use blockchain fee for 0 amount tx + val fee = getInchFee(quotesData) + val isFeeEnough = checkFeeIsEnough( + fee = fee, + spendAmount = amount, + networkId = networkId, + fromToken = fromToken, + ) + val isBalanceEnough = isBalanceEnough(fromToken, amount, fee) val preparedSwapConfigState = PreparedSwapConfigState( isAllowedToSpend = isAllowedToSpend, isBalanceEnough = isBalanceEnough, isFeeEnough = isFeeEnough, ) return if (isAllowedToSpend && isBalanceEnough && isFeeEnough) { - loadSwapData( + // if enough balance, fee and spend was allowed, request swap data + val swapData = loadSwapData( networkId = networkId, fromTokenAddress = fromTokenAddress, toTokenAddress = toTokenAddress, fromToken = fromToken, toToken = toToken, amount = amount, - preparedSwapConfigState = preparedSwapConfigState, ) + if (swapData is SwapState.QuotesLoadedState) { + swapData.copy(preparedSwapConfigState = preparedSwapConfigState) + } else { + swapData + } } else { - loadQuoteData( - networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - amount = amount, - fromToken = fromToken, - toToken = toToken, - preparedSwapConfigState = preparedSwapConfigState, - ) + if (quotesData is SwapState.QuotesLoadedState) { + quotesData.copy(preparedSwapConfigState = preparedSwapConfigState) + } else { + quotesData + } } } @@ -195,8 +211,7 @@ internal class SwapInteractorImpl @Inject constructor( amountToSwap: String, ): TxState { val amount = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format, use only digits" } - val estimatedGas = - increaseByPercents(TWENTY_FIVE_PERCENTS, swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS) + val estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS val fee = transactionManager.calculateFee( networkId = networkId, gasPrice = swapData.transaction.gasPrice, @@ -250,6 +265,12 @@ internal class SwapInteractorImpl @Inject constructor( } } + private fun getInchFee(quotesData: SwapState): BigDecimal? { + return if (quotesData is SwapState.QuotesLoadedState) { + quotesData.feeRaw + } else null + } + private fun selectToToken( initialToken: Currency, tokensInWallet: List, @@ -294,25 +315,6 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun getAndUpdateFee(networkId: String, fromToken: Currency): BigDecimal? { - val lastFee = cache.getLastFeeForNetwork(networkId) - if (lastFee == null) { - if (userWalletManager.getNativeTokenBalance(networkId)?.value?.compareTo(BigDecimal.ZERO) == 0) { - return null - } - val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken)) - val fee = transactionManager.getFee( - networkId, - BigDecimal.ZERO, - cryptoCurrencyConverter.convert(fromToken), - transactionData.toAddress, - ).value - cache.cacheLastFeeForNetwork(fee, networkId) - return fee - } - return lastFee - } - private suspend fun checkAllowance(networkId: String, fromTokenAddress: String): Boolean { val allowance = repository.checkTokensSpendAllowance( networkId = networkId, @@ -351,7 +353,6 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fromToken: Currency, toToken: Currency, - preparedSwapConfigState: PreparedSwapConfigState, ): SwapState { repository.findBestQuote( networkId = networkId, @@ -380,7 +381,7 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenAmount = quoteDataModel.fromTokenAmount, toTokenAmount = quoteDataModel.toTokenAmount, formattedFee = formattedFee, - preparedSwapConfigState = preparedSwapConfigState, + feeRaw = fee, swapDataModel = null, ) return updatePermissionState( @@ -422,7 +423,6 @@ internal class SwapInteractorImpl @Inject constructor( fromToken: Currency, toToken: Currency, amount: SwapAmount, - preparedSwapConfigState: PreparedSwapConfigState, ): SwapState { repository.prepareSwapTransaction( networkId = networkId, @@ -452,8 +452,8 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenAmount = swapData.fromTokenAmount, toTokenAmount = swapData.toTokenAmount, formattedFee = formattedFee, - preparedSwapConfigState = preparedSwapConfigState, swapDataModel = swapData, + feeRaw = fee, ) return swapState.copy( permissionState = PermissionDataState.Empty, @@ -472,7 +472,7 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, formattedFee: String, - preparedSwapConfigState: PreparedSwapConfigState, + feeRaw: BigDecimal, swapDataModel: SwapDataModel?, ): SwapState.QuotesLoadedState { val appCurrency = userWalletManager.getUserAppCurrency() @@ -511,9 +511,9 @@ internal class SwapInteractorImpl @Inject constructor( toRate = rates[toToken.id] ?: 0.0, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), - preparedSwapConfigState = preparedSwapConfigState, swapDataModel = swapDataModel, tangemFee = getTangemFee(), + feeRaw = feeRaw, ) } @@ -639,7 +639,7 @@ internal class SwapInteractorImpl @Inject constructor( private const val DEFAULT_GAS = 300000 private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" private const val TWENTY_FIVE_PERCENTS = 25 - private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.5 + private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.4 private const val USDT_SYMBOL = "USDT" private const val USDC_SYMBOL = "USDC" private const val INFINITY_SYMBOL = "∞" diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 1515683e56..deace8a843 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -5,6 +5,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.ApproveModel import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import java.math.BigDecimal sealed interface SwapState { @@ -14,10 +15,15 @@ sealed interface SwapState { val fee: String, val priceImpact: Float, val networkCurrency: String, - val preparedSwapConfigState: PreparedSwapConfigState, + val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState( + isAllowedToSpend = false, + isBalanceEnough = false, + isFeeEnough = false, + ), val permissionState: PermissionDataState = PermissionDataState.Empty, val swapDataModel: SwapDataModel? = null, val tangemFee: Double, + val feeRaw: BigDecimal, ) : SwapState data class EmptyAmountState( diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 684ec4d57c..287b9e6ad6 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { /** Other libraries */ implementation(Library.composeShimmer) implementation(Library.kotlinSerialization) + implementation(Library.timber) /** DI */ implementation(Library.hilt) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 1b52c30bb6..0d9a788af5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -41,6 +41,7 @@ import com.tangem.core.ui.components.SimpleOkDialog import com.tangem.core.ui.components.SmallInfoCard import com.tangem.core.ui.components.SmallInfoCardWithDisclaimer import com.tangem.core.ui.components.SmallInfoCardWithWarning +import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.WarningCard import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.keyboardAsState @@ -343,6 +344,7 @@ private fun SwapWarnings( // ) // } } + SpacerH8() } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index ddf5f6876c..8851cc69d2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -1,6 +1,5 @@ package com.tangem.feature.swap.viewmodels -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -32,6 +31,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import timber.log.Timber import java.text.DecimalFormat import java.text.NumberFormat import java.util.* @@ -122,7 +122,7 @@ internal class SwapViewModel @Inject constructor( ) } .onFailure { - Log.e("SwapViewModel", it.message ?: it.cause.toString()) + Timber.e(it) } } } @@ -198,11 +198,13 @@ internal class SwapViewModel @Inject constructor( ) } is SwapState.SwapError -> { + Timber.e("SwapError when loading quotes ${swapState.error}") uiState = stateBuilder.mapError(uiState, swapState.error) { startLoadingQuotesFromLastState() } } } }, onError = { + Timber.e("Error when loading quotes: $it") uiState = stateBuilder.addWarning(uiState, null) { startLoadingQuotesFromLastState() } }, )