Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-26 11:32:43 +04:00
parent 6d9c467b20
commit 40f24bd65b
13 changed files with 191 additions and 84 deletions

View file

@ -16,6 +16,7 @@ import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -47,6 +48,14 @@ class DefaultCustomTokenInteractor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) : CustomTokenInteractor {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
override suspend fun findToken(address: String, blockchain: Blockchain): FoundToken {
return featureRepository.findToken(
address = address,
@ -219,21 +228,13 @@ class DefaultCustomTokenInteractor(
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
scope.launch {
userWalletsListManager.update(
userWalletId = userWalletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList)
val networks = currencyList.map { it.network }.toSet()
networksRepository.getNetworkStatusesSync(
userWalletId = userWalletId,
networks = networks,
refresh = true,
)
addCryptoCurrenciesUseCase(userWalletId, currencyList)
}
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.CryptoCurrency
@ -37,6 +38,14 @@ import timber.log.Timber
@Suppress("LargeClass")
object TokensMiddleware {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
val tokensMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
@ -372,22 +381,13 @@ object TokensMiddleware {
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
scope.launch {
userWalletsListManager.update(
userWalletId = userWalletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList)
networksRepository.getNetworkStatusesSync(
userWalletId = userWalletId,
networks = currencyList.map(CryptoCurrency::network).toSet(),
refresh = true,
)
addCryptoCurrenciesUseCase(userWalletId, currencyList)
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
@ -46,6 +47,11 @@ class DerivationManagerImpl(
private val networksRepository: NetworksRepository,
) : DerivationManager {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine { continuation ->
val blockchain = Blockchain.fromNetworkId(currency.networkId)
val card = appStateHolder.getActualCard()
@ -164,15 +170,8 @@ class DerivationManagerImpl(
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
currenciesRepository.addCurrencies(
userWalletId,
listOf(cryptoCurrency),
)
networksRepository.getNetworkStatusesSync(
userWalletId = userWalletId,
networks = setOf(cryptoCurrency.network),
refresh = true,
)
addCryptoCurrenciesUseCase(userWalletId, cryptoCurrency)
}
private fun convertCurrency(

View file

@ -1,9 +1,6 @@
package com.tangem.data.tokens.utils
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.*
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
@ -42,7 +39,7 @@ internal class NetworkStatusFactory {
private fun formatAmounts(
amounts: Set<CryptoCurrencyAmount>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, NetworkStatus.AmountStatus> {
): Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus> {
return currencies.associate { currency ->
val amount = when (currency) {
is CryptoCurrency.Coin -> {
@ -56,11 +53,12 @@ internal class NetworkStatusFactory {
}
}
}
if (amount == null) {
Timber.e("Unable to find amount for cryptoCurrency: $currency")
currency.id to NetworkStatus.UnreachableAmount
Timber.w("Unable to find amount for cryptocurrency: $currency")
currency.id to CryptoCurrencyAmountStatus.NotFound
} else {
currency.id to NetworkStatus.LoadedAmount(amount.value)
currency.id to CryptoCurrencyAmountStatus.Loaded(amount.value)
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.tokens.model
import java.math.BigDecimal
/**
* Represents possible statuses of cryptocurrency amount.
*/
sealed class CryptoCurrencyAmountStatus {
data class Loaded(val value: BigDecimal) : CryptoCurrencyAmountStatus()
object NotFound : CryptoCurrencyAmountStatus()
}

View file

@ -42,7 +42,7 @@ data class NetworkStatus(
*/
data class Verified(
val address: NetworkAddress,
val amounts: Map<CryptoCurrency.ID, AmountStatus>,
val amounts: Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus>,
val pendingTransactions: Map<CryptoCurrency.ID, Set<TxHistoryItem>>,
) : Status()
@ -58,14 +58,4 @@ data class NetworkStatus(
val amountToCreateAccount: BigDecimal,
val errorMessage: String,
) : Status()
/**
* Represents possible statuses of amount.
*
* This sealed class includes states as LoadedAmount, UnreachableAmount.
*/
sealed class AmountStatus
data class LoadedAmount(val value: BigDecimal) : AmountStatus()
object UnreachableAmount : AmountStatus()
}

View file

@ -0,0 +1,98 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.toNonEmptyListOrNull
import com.tangem.domain.tokens.error.AddCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWalletId
/**
* A use case for adding multiple cryptocurrencies to a user's wallet.
*
* This use case interacts with the underlying repositories to both add currencies and refresh
* network statuses, particularly after the addition of new tokens.
*/
// TODO: Add tests
class AddCryptoCurrenciesUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
) {
/**
* Adds a [currency] to the wallet identified by [userWalletId].
*
* After successfully adding a currency, it also refreshes the networks for tokens
* that are being added and have corresponding coins in the existing currencies list.
*
* @param userWalletId The ID of the user's wallet.
* @param currency Cryptocurrency to add.
* @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation.
*/
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<AddCurrencyError, Unit> {
return invoke(userWalletId, listOf(currency))
}
/**
* Adds a list of [currencies] to the wallet identified by [userWalletId].
*
* After successfully adding currencies, it also refreshes the networks for tokens
* that are being added and have corresponding coins in the existing currencies list.
*
* @param userWalletId The ID of the user's wallet.
* @param currencies The list of cryptocurrencies to add.
* @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation.
*/
suspend operator fun invoke(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<AddCurrencyError, Unit> = either {
val existingCurrencies = catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) {
raise(AddCurrencyError.DataError(it))
}
val currenciesToAdd = currencies
.filterNot(existingCurrencies::contains)
.toNonEmptyListOrNull()
?: return@either
catch({ currenciesRepository.addCurrencies(userWalletId, currenciesToAdd) }) {
raise(AddCurrencyError.DataError(it))
}
refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies)
}
/**
* Refreshes the network statuses for tokens that have corresponding coins in the
* [existingCurrencies] list.
*/
private suspend fun Raise<AddCurrencyError>.refreshUpdatedNetworks(
userWalletId: UserWalletId,
currenciesToAdd: List<CryptoCurrency>,
existingCurrencies: List<CryptoCurrency>,
) {
val networksToUpdate = currenciesToAdd
.asSequence()
.filterIsInstance<CryptoCurrency.Token>()
.filter { hasCoinForToken(existingCurrencies, it) }
.mapTo(hashSetOf(), CryptoCurrency.Token::network)
catch({ networksRepository.getNetworkStatusesSync(userWalletId, networksToUpdate, refresh = true) }) {
raise(AddCurrencyError.DataError(it))
}
}
/**
* Determines if the [existingCurrencies] list contains a coin that corresponds
* to the given [token].
*/
private fun hasCoinForToken(existingCurrencies: List<CryptoCurrency>, token: CryptoCurrency.Token): Boolean {
return existingCurrencies.any { currency ->
currency is CryptoCurrency.Coin && currency.network == token.network
}
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.tokens.error
sealed class AddCurrencyError {
data class DataError(val cause: Throwable) : AddCurrencyError()
}

View file

@ -1,9 +1,6 @@
package com.tangem.domain.tokens.operations
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.*
import java.math.BigDecimal
internal class CurrencyStatusOperations(
@ -39,16 +36,16 @@ internal class CurrencyStatusOperations(
)
private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status {
val amount = status.amounts[currency.id]?.let {
when (it) {
is NetworkStatus.UnreachableAmount -> {
return CryptoCurrencyStatus.NoAmount(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate)
}
is NetworkStatus.LoadedAmount -> {
it.value
}
val amount = when (val amount = status.amounts[currency.id]) {
null -> {
return CryptoCurrencyStatus.Loading
}
} ?: return CryptoCurrencyStatus.Loading
is CryptoCurrencyAmountStatus.NotFound -> {
return CryptoCurrencyStatus.NoAmount(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate)
}
is CryptoCurrencyAmountStatus.Loaded -> amount.value
}
val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty()
val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet)

View file

@ -116,7 +116,10 @@ interface CurrenciesRepository {
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun getMultiCurrencyWalletCurrenciesSync(userWalletId: UserWalletId, refresh: Boolean): List<CryptoCurrency>
suspend fun getMultiCurrencyWalletCurrenciesSync(
userWalletId: UserWalletId,
refresh: Boolean = false,
): List<CryptoCurrency>
/**
* Retrieves the cryptocurrency for a specific multi-currency user wallet.

View file

@ -2,6 +2,7 @@ package com.tangem.domain.tokens.mock
import arrow.core.NonEmptySet
import arrow.core.nonEmptySetOf
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
@ -61,9 +62,9 @@ internal object MockNetworks {
get() = networkStatus1.copy(
value = NetworkStatus.Verified(
amounts = mapOf(
MockTokens.token1.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token2.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token3.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token1.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
MockTokens.token2.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
MockTokens.token3.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
),
pendingTransactions = emptyMap(),
address = NetworkAddress.Single(defaultAddress = "mock"),
@ -74,9 +75,9 @@ internal object MockNetworks {
get() = networkStatus2.copy(
value = NetworkStatus.Verified(
amounts = mapOf(
MockTokens.token4.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token5.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token6.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token4.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
MockTokens.token5.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
MockTokens.token6.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
),
pendingTransactions = emptyMap(),
address = NetworkAddress.Single(defaultAddress = "mock"),
@ -87,10 +88,10 @@ internal object MockNetworks {
get() = networkStatus3.copy(
value = NetworkStatus.Verified(
amounts = mapOf(
MockTokens.token7.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token8.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token9.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token10.id to NetworkStatus.LoadedAmount(BigDecimal.TEN),
MockTokens.token7.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
MockTokens.token8.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
MockTokens.token9.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
MockTokens.token10.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
),
pendingTransactions = emptyMap(),
address = NetworkAddress.Single(defaultAddress = "mock"),

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens.mock
import arrow.core.nonEmptyListOf
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkStatus
import java.math.BigDecimal
@ -110,7 +111,7 @@ internal object MockTokensStates {
.first { it.network == status.currency.network }
val amount = (
(networkStatus.value as NetworkStatus.Verified).amounts[status.currency.id]!!
as? NetworkStatus.LoadedAmount
as? CryptoCurrencyAmountStatus.Loaded
)?.value ?: BigDecimal.ZERO
val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId }
val fiatAmount = amount * quote.fiatRate

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
@ -36,6 +37,11 @@ internal class SwapInteractorImpl @Inject constructor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) : SwapInteractor {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
private val swapCurrencyConverter = SwapCurrencyConverter()
private val amountFormatter = AmountFormatter()
private var derivationPath: String? = null
@ -164,7 +170,7 @@ internal class SwapInteractorImpl @Inject constructor(
): SwapState {
syncWalletBalanceForTokens(networkId, listOf(fromToken, toToken))
val amountDecimal = toBigDecimalOrNull(amountToSwap)
if (amountDecimal == null || amountDecimal.compareTo(BigDecimal.ZERO) == 0) {
if (amountDecimal == null || amountDecimal.signum() == 0) {
return createEmptyAmountState(networkId, fromToken, toToken)
}
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken))
@ -283,15 +289,7 @@ internal class SwapInteractorImpl @Inject constructor(
private suspend fun getAndAddCryptoCurrency(userWallet: UserWallet, currency: Currency, network: Network) {
repository.getCryptoCurrency(userWallet, currency, network)?.let { cryptoCurrency ->
currenciesRepository.addCurrencies(
userWallet.walletId,
listOf(cryptoCurrency),
)
networksRepository.getNetworkStatusesSync(
userWalletId = userWallet.walletId,
networks = setOf(cryptoCurrency.network),
refresh = true,
)
addCryptoCurrenciesUseCase(userWallet.walletId, cryptoCurrency)
}
}
@ -567,7 +565,7 @@ internal class SwapInteractorImpl @Inject constructor(
quotesLoadedState: SwapState.QuotesLoadedState,
): SwapState.QuotesLoadedState {
// if token balance ZERO not show permission state to avoid user to spend money for fee
val isTokenZeroBalance = getTokenBalance(networkId, fromToken).value.compareTo(BigDecimal.ZERO) == 0
val isTokenZeroBalance = getTokenBalance(networkId, fromToken).value.signum() == 0
if (isTokenZeroBalance) {
return quotesLoadedState.copy(
permissionState = PermissionDataState.Empty,
@ -711,7 +709,7 @@ internal class SwapInteractorImpl @Inject constructor(
return false
}
val nativeTokenBalance = userWalletManager.getNativeTokenBalance(networkId, derivationPath)
val percentsToFeeIncrease = BigDecimal.valueOf(INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT)
val percentsToFeeIncrease = BigDecimal.ONE
return when (fromToken) {
is Currency.NativeToken -> {
nativeTokenBalance?.let { balance ->
@ -768,6 +766,8 @@ internal class SwapInteractorImpl @Inject constructor(
private const val DEFAULT_SLIPPAGE = 2
private const val ZERO_BALANCE = "0"
private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
@Suppress("UnusedPrivateMember")
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
private const val USDT_SYMBOL = "USDT"