Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-23 13:16:25 +03:00
commit f80c30c206
14 changed files with 61 additions and 91 deletions

View file

@ -22,6 +22,7 @@ dependencies {
implementation(project(":data:common"))
implementation(project(":libs:auth"))
implementation(project(":libs:crypto"))
implementation(projects.domain.tokens.models)
implementation(deps.material)

View file

@ -22,6 +22,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
/** Data */
@ -32,5 +33,6 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Approver
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.PairsRequestBody
@ -15,8 +16,10 @@ import com.tangem.datasource.api.oneinch.errors.OneIncResponseException
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.SwapRepository
@ -40,6 +43,7 @@ internal class SwapRepositoryImpl @Inject constructor(
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val configManager: ConfigManager,
private val walletManagersFacade: WalletManagersFacade,
private val walletsStateHolder: WalletsStateHolder,
) : SwapRepository {
private val tokensConverter = TokensConverter()
@ -47,6 +51,7 @@ internal class SwapRepositoryImpl @Inject constructor(
private val leastTokenInfoConverter = LeastTokenInfoConverter()
private val swapPairInfoConverter = SwapPairInfoConverter()
private val swapProviderConverter = SwapProviderConverter()
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
override suspend fun getPairs(
initialCurrency: LeastTokenInfo,
@ -304,6 +309,23 @@ internal class SwapRepositoryImpl @Inject constructor(
)
}
override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
return requireNotNull(
cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = requireNotNull(
walletsStateHolder.userWalletsListManager
?.selectedUserWalletSync
?.scanResponse
?.derivationStyleProvider,
),
),
)
}
companion object {
// TODO("get this ids from blockchain enum later")
private const val OPTIMISM_ID = "optimistic-ethereum"

View file

@ -6,6 +6,7 @@ import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.feature.swap.SwapRepositoryImpl
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -29,6 +30,7 @@ class SwapDataModule {
coroutineDispatcher: CoroutineDispatcherProvider,
configManager: ConfigManager,
walletManagerFacade: WalletManagersFacade,
walletsStateHolder: WalletsStateHolder,
): SwapRepository {
return SwapRepositoryImpl(
tangemTechApi = tangemTechApi,
@ -38,6 +40,7 @@ class SwapDataModule {
coroutineDispatcher = coroutineDispatcher,
configManager = configManager,
walletManagersFacade = walletManagerFacade,
walletsStateHolder = walletsStateHolder,
)
}
}

View file

@ -25,14 +25,6 @@ interface SwapInteractor {
*/
suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress
/**
* Find specific token by id, null if not found
*
* @param id token id
* @return [CryptoCurrency] or null
*/
fun findTokenById(id: String): CryptoCurrency?
/**
* Gives permission to swap, this starts scan card process
*

View file

@ -6,8 +6,10 @@ 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.Network
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.cache.SwapDataCache
@ -15,7 +17,6 @@ import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
@ -44,6 +45,7 @@ internal class SwapInteractorImpl @Inject constructor(
private val walletFeatureToggles: WalletFeatureToggles,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
private val quotesRepository: QuotesRepository,
) : SwapInteractor {
// TODO: Move to DI
@ -187,14 +189,6 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
@Deprecated("used in old swap mechanism")
override fun findTokenById(id: String): CryptoCurrency? {
val tokensInWallet = cache.getInWalletTokens()
val loadedTokens = cache.getLoadedTokens()
return tokensInWallet.firstOrNull { it.token.id.value == id }?.token
?: loadedTokens.firstOrNull { it.token.id.value == id }?.token
}
@Deprecated("used in old swap mechanism")
override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState {
val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) {
@ -389,32 +383,6 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
@Suppress("UnusedPrivateMember")
@Deprecated("used in old swap mechanism")
private fun getTokensWithBalance(
tokens: List<Currency>,
balances: Map<String, SwapAmount>,
rates: Map<String, Double>,
appCurrency: ProxyFiatCurrency,
): List<TokenWithBalance> {
return tokens.map {
val balance = balances[it.symbol]
TokenWithBalance(
token = it,
tokenBalanceData = TokenBalanceData(
amount = balance?.let { amount ->
amountFormatter.formatSwapAmountToUI(amount, it.symbol)
},
amountEquivalent = balance?.value?.toFiatString(
rateValue = rates[it.id]?.toBigDecimal() ?: BigDecimal.ZERO,
fiatCurrencyName = appCurrency.symbol,
formatWithSpaces = true,
),
),
)
}
}
private suspend fun isAllowedToSpend(networkId: String, fromToken: CryptoCurrency, amount: SwapAmount): Boolean {
if (fromToken is CryptoCurrency.Coin) return true
return getSelectedWalletSyncUseCase().fold(
@ -537,9 +505,9 @@ internal class SwapInteractorImpl @Inject constructor(
private suspend fun getFormattedFiatFees(networkId: String, vararg fees: BigDecimal): List<String> {
val appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
val rates = repository.getRates(appCurrency.code, listOf(nativeToken.id))
return rates[nativeToken.id]?.toBigDecimal()?.let { rate ->
val nativeToken = repository.getNativeTokenForNetwork(networkId)
val rates = getQuotes(nativeToken.id)
return rates[nativeToken.id]?.fiatRate?.let { rate ->
fees.map { fee ->
" (${fee.toFiatString(rate, appCurrency.symbol, true)})"
}
@ -627,30 +595,27 @@ internal class SwapInteractorImpl @Inject constructor(
): SwapState.QuotesLoadedState {
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
val appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
val rates = repository.getRates(
appCurrency.code,
listOf(fromToken.network.backendId, toToken.network.backendId, nativeToken.id),
)
val nativeToken = repository.getNativeTokenForNetwork(networkId)
val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id)
return SwapState.QuotesLoadedState(
fromTokenInfo = TokenSwapInfo(
tokenAmount = fromTokenAmount,
cryptoCurrencyStatus = fromTokenStatus,
amountFiat = rates[fromToken.network.backendId]?.toBigDecimal()?.multiply(fromTokenAmount.value)
amountFiat = rates[fromToken.id]?.fiatRate?.multiply(fromTokenAmount.value)
?: BigDecimal.ZERO,
),
toTokenInfo = TokenSwapInfo(
tokenAmount = toTokenAmount,
cryptoCurrencyStatus = toTokenStatus,
amountFiat = rates[toToken.network.backendId]?.toBigDecimal()?.multiply(toTokenAmount.value)
amountFiat = rates[toToken.id]?.fiatRate?.multiply(toTokenAmount.value)
?: BigDecimal.ZERO,
),
priceImpact = calculatePriceImpact(
fromTokenAmount = fromTokenAmount.value,
fromRate = rates[fromToken.network.backendId] ?: 0.0,
fromRate = rates[fromToken.id]?.fiatRate?.toDouble() ?: 0.0,
toTokenAmount = toTokenAmount.value,
toRate = rates[toToken.network.backendId] ?: 0.0,
toRate = rates[toToken.id]?.fiatRate?.toDouble() ?: 0.0,
),
networkCurrency = userWalletManager.getNetworkCurrency(networkId),
swapDataModel = swapStateData,
@ -687,7 +652,7 @@ internal class SwapInteractorImpl @Inject constructor(
val feeData = transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
currencyToSend = userWalletManager.getNativeTokenForNetwork(networkId),
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
destinationAddress = getTokenAddress(fromToken),
increaseBy = INCREASE_GAS_LIMIT_BY,
data = transactionData,
@ -863,6 +828,14 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map<CryptoCurrency.ID, Quote> {
val set = quotesRepository.getQuotesSync(ids.toSet(), false)
return ids
.mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } }
.toMap()
}
companion object {
private const val DEFAULT_SLIPPAGE = 2

View file

@ -78,4 +78,6 @@ interface SwapRepository {
providerId: String,
rateType: RateType,
): ExchangeQuote
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
}

View file

@ -1,20 +1,15 @@
package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress
import java.math.BigDecimal
interface SwapDataCache {
fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>)
fun cacheInWalletTokens(tokens: List<TokenWithBalanceExpress>)
fun cacheLoadedTokens(tokens: List<TokenWithBalanceExpress>)
fun cacheBalances(networkId: String, derivationPath: String?, balances: Map<String, SwapAmount>)
fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String)
fun getAvailableTokens(networkId: String): List<Currency>
fun getInWalletTokens(): List<TokenWithBalanceExpress>
fun getLoadedTokens(): List<TokenWithBalanceExpress>
fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount?
fun getLastFeeForNetwork(networkId: String): BigDecimal?
}

View file

@ -1,22 +1,14 @@
package com.tangem.feature.swap.domain.cache
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress
import java.math.BigDecimal
class SwapDataCacheImpl : SwapDataCache {
private val availableTokensForNetwork: MutableMap<String, List<Currency>> = mutableMapOf()
private val feesForNetworks: MutableMap<String, BigDecimal> = mutableMapOf()
private val tokensBalances: MutableMap<String, Map<String, SwapAmount>> = mutableMapOf()
private val lastInWalletTokens = mutableListOf<TokenWithBalanceExpress>()
private val lastLoadedTokens = mutableListOf<TokenWithBalanceExpress>()
override fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) {
feesForNetworks[networkId] = fee
}
override fun cacheInWalletTokens(tokens: List<TokenWithBalanceExpress>) {
lastInWalletTokens.clear()
lastInWalletTokens.addAll(tokens)
@ -43,18 +35,6 @@ class SwapDataCacheImpl : SwapDataCache {
tokensBalances[createKeyFrom(networkId, derivationPath)] = balances
}
override fun cacheAvailableToSwapTokens(networkId: String, tokens: List<Currency>) {
availableTokensForNetwork[networkId] = tokens
}
override fun getLastFeeForNetwork(networkId: String): BigDecimal? {
return feesForNetworks[networkId]
}
override fun getAvailableTokens(networkId: String): List<Currency> {
return availableTokensForNetwork.getOrElse(networkId) { emptyList() }
}
private fun createKeyFrom(networkId: String, derivationPath: String?): String {
return "$networkId;$derivationPath"
}

View file

@ -35,6 +35,7 @@ class SwapDomainModule {
walletFeatureToggles: WalletFeatureToggles,
@SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
@SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
quotesRepository: QuotesRepository,
): SwapInteractor {
return SwapInteractorImpl(
transactionManager = transactionManager,
@ -47,6 +48,7 @@ class SwapDomainModule {
walletFeatureToggles = walletFeatureToggles,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase,
quotesRepository = quotesRepository,
)
}

View file

@ -34,7 +34,7 @@ import java.math.RoundingMode
/**
* State builder creates a specific states for SwapScreen
*/
@Suppress("LargeClass")
@Suppress("LargeClass", "TooManyFunctions")
internal class StateBuilder(
private val actions: UiActions,
private val isBalanceHiddenProvider: Provider<Boolean>,

View file

@ -2,19 +2,15 @@ package com.tangem.feature.swap.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig
import com.tangem.feature.swap.presentation.R
@OptIn(ExperimentalLayoutApi::class)
@Composable

View file

@ -530,7 +530,7 @@ internal class SwapViewModel @Inject constructor(
}
}
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun createUiActions(): UiActions {
return UiActions(
onSearchEntered = { onSearchEntered(it) },

View file

@ -1,10 +1,12 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
dependencies {
/** Coroutines */
implementation(deps.kotlin.coroutines)
implementation(projects.domain.tokens.models)
}