Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-02 15:57:52 +03:00
parent 21f575fb13
commit 95fea5388f
7 changed files with 81 additions and 48 deletions

View file

@ -92,7 +92,6 @@ data class WalletData(
)
walletWarnings.add(warning)
}
}
private fun assembleTokenWarnings(walletWarnings: MutableList<WalletWarning>) = with(currency) {

View file

@ -35,8 +35,24 @@ internal class SwapRepositoryImpl @Inject constructor(
private val approveConverter = ApproveConverter()
override suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double> {
// 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"
}
}

View file

@ -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<Currency>,
@ -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 = ""

View file

@ -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(

View file

@ -74,6 +74,7 @@ dependencies {
/** Other libraries */
implementation(Library.composeShimmer)
implementation(Library.kotlinSerialization)
implementation(Library.timber)
/** DI */
implementation(Library.hilt)

View file

@ -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()
}
}
}

View file

@ -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() }
},
)