Updated on 2026-08-14
This commit is contained in:
commit
f80c30c206
14 changed files with 61 additions and 91 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -78,4 +78,6 @@ interface SwapRepository {
|
|||
providerId: String,
|
||||
rateType: RateType,
|
||||
): ExchangeQuote
|
||||
|
||||
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -530,7 +530,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun createUiActions(): UiActions {
|
||||
return UiActions(
|
||||
onSearchEntered = { onSearchEntered(it) },
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue