Updated on 2026-08-14
This commit is contained in:
parent
21f575fb13
commit
95fea5388f
7 changed files with 81 additions and 48 deletions
|
|
@ -92,7 +92,6 @@ data class WalletData(
|
|||
)
|
||||
walletWarnings.add(warning)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun assembleTokenWarnings(walletWarnings: MutableList<WalletWarning>) = with(currency) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "∞"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ dependencies {
|
|||
/** Other libraries */
|
||||
implementation(Library.composeShimmer)
|
||||
implementation(Library.kotlinSerialization)
|
||||
implementation(Library.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(Library.hilt)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue