Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-09 17:41:42 +04:00
parent 5b95c59a92
commit 50806204ab
9 changed files with 357 additions and 92 deletions

View file

@ -24,6 +24,7 @@ import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.replaceBy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@ -109,6 +110,23 @@ internal class DefaultAccountsCRUDRepository(
walletAccountsSaver.pushAndStore(userWalletId = userWallet.walletId, response = accountsResponse)
}
override suspend fun saveAccount(account: Account.CryptoPortfolio) {
val store = getAccountsResponseStore(userWalletId = account.userWalletId)
val converter = convertersContainer.createCryptoPortfolioConverter(userWalletId = account.userWalletId)
val newAccountDTO = converter.convertBack(value = account)
store.updateData { response ->
response ?: return@updateData response
response.copy(
accounts = response.accounts.toMutableList().apply {
replaceBy(newAccountDTO) { it.id == newAccountDTO.id }
},
)
}
}
override suspend fun getTotalAccountsCountSync(userWalletId: UserWalletId): Option<Int> = option {
val accountListResponse = getAccountsResponseSync(userWalletId = userWalletId)

View file

@ -74,77 +74,18 @@ internal class DefaultCurrenciesRepository(
userTokensSaver.storeAndPush(userWalletId, response)
}
override suspend fun saveNewCurrenciesList(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
override suspend fun saveCurrenciesLocal(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
withContext(dispatchers.io) {
val savedResponse = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action." },
)
val newCurrencies = populateCurrenciesWithMissedCoins(currencies)
val updatedResponse = savedResponse.copy(
tokens = newCurrencies.map(userTokensResponseFactory::createResponseToken),
)
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = updatedResponse,
tokens = currencies.map(userTokensResponseFactory::createResponseToken),
)
fetchExpressAssetsByNetworkIds(
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
userTokens = updatedResponse,
)
}
}
override suspend fun addCurrencies(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): List<CryptoCurrency> = withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
val currenciesToAdd = filterAlreadyAddedCurrencies(
savedCurrencies = savedCurrencies.tokens,
currenciesToAdd = populateCurrenciesWithMissedCoins(currencies = currencies),
)
val updatedResponse = savedCurrencies.copy(
tokens = savedCurrencies.tokens + currenciesToAdd.map(userTokensResponseFactory::createResponseToken),
)
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = updatedResponse,
)
fetchExpressAssetsByNetworkIds(
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
userTokens = updatedResponse,
)
currenciesToAdd
}
override suspend fun saveNewCurrenciesListCache(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
withContext(dispatchers.io) {
val savedResponse = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action." },
)
val newCurrencies = populateCurrenciesWithMissedCoins(currencies)
val updatedResponse = savedResponse.copy(
tokens = newCurrencies.map(userTokensResponseFactory::createResponseToken),
)
userTokensSaver.store(
userWalletId = userWalletId,
response = updatedResponse,
)
userTokensSaver.store(userWalletId = userWalletId, response = updatedResponse)
fetchExpressAssetsByNetworkIds(
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
@ -551,6 +492,10 @@ internal class DefaultCurrenciesRepository(
}
}
override fun createCoinCurrency(network: Network): CryptoCurrency.Coin {
return cryptoCurrencyFactory.createCoin(network = network)
}
override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token {
return cryptoCurrencyFactory.createToken(
cryptoCurrency = cryptoCurrency,

View file

@ -68,6 +68,13 @@ interface AccountsCRUDRepository {
*/
suspend fun saveAccounts(accountList: AccountList)
/**
* Save account
*
* @param account account to be saved
*/
suspend fun saveAccount(account: Account.CryptoPortfolio)
/**
* Retrieves the total count of accounts associated with a specific user wallet including archived accounts
*

View file

@ -24,6 +24,7 @@ dependencies {
api(projects.domain.networks)
api(projects.domain.staking)
api(projects.domain.tokens)
api(projects.domain.wallets)
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)

View file

@ -1,11 +1,22 @@
package com.tangem.domain.account.status.di
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.utils.NetworksCleaner
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.utils.StakingCleaner
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -37,4 +48,34 @@ internal object AccountStatusUseCaseModule {
): GetAccountCurrencyStatusUseCase {
return GetAccountCurrencyStatusUseCase(singleAccountStatusListSupplier = singleAccountStatusListSupplier)
}
@Provides
@Singleton
fun provideSaveCryptoCurrenciesUseCase(
singleAccountListSupplier: SingleAccountListSupplier,
accountsCRUDRepository: AccountsCRUDRepository,
currenciesRepository: CurrenciesRepository,
derivationsRepository: DerivationsRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
networksCleaner: NetworksCleaner,
stakingCleaner: StakingCleaner,
dispatchers: CoroutineDispatcherProvider,
): SaveCryptoCurrenciesUseCase {
return SaveCryptoCurrenciesUseCase(
singleAccountListSupplier = singleAccountListSupplier,
accountsCRUDRepository = accountsCRUDRepository,
currenciesRepository = currenciesRepository,
derivationsRepository = derivationsRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
networksCleaner = networksCleaner,
stakingCleaner = stakingCleaner,
dispatchers = dispatchers,
)
}
}

View file

@ -0,0 +1,269 @@
package com.tangem.domain.account.status.usecase
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.catch
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.core.utils.eitherOn
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.utils.NetworksCleaner
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.utils.StakingCleaner
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import timber.log.Timber
/**
* Use case for saving crypto currencies to a specific account.
*
* @property singleAccountListSupplier Supplier to get account details.
* @property currenciesRepository Repository for managing currencies.
* @property derivationsRepository Repository for deriving public keys.
* @property multiNetworkStatusFetcher Fetcher for updating network statuses.
* @property multiQuoteStatusFetcher Fetcher for updating quote statuses.
* @property multiYieldBalanceFetcher Fetcher for updating yield balances.
* @property stakingIdFactory Factory for creating staking IDs.
* @property networksCleaner Cleaner for removing obsolete network data.
* @property stakingCleaner Cleaner for removing obsolete staking data.
* @property dispatchers Coroutine dispatchers for managing threading.
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
class SaveCryptoCurrenciesUseCase(
private val singleAccountListSupplier: SingleAccountListSupplier,
private val accountsCRUDRepository: AccountsCRUDRepository,
private val currenciesRepository: CurrenciesRepository,
private val derivationsRepository: DerivationsRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
private val stakingIdFactory: StakingIdFactory,
private val networksCleaner: NetworksCleaner,
private val stakingCleaner: StakingCleaner,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
accountId: AccountId,
add: List<CryptoCurrency>,
remove: List<CryptoCurrency>,
): Either<Throwable, Unit> = eitherOn(dispatchers.default) {
if (add.isEmpty() && remove.isEmpty()) {
Timber.d("No currencies to add or remove, skipping")
return@eitherOn
}
val userWalletId = accountId.userWalletId
withContext(NonCancellable) {
val account = getAccount(accountId = accountId)
val modifiedCurrencyList = account.cryptoCurrencies.modify(add = add, remove = remove)
saveAccount(
account = account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()),
)
derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
val jobs = refreshBalances(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) +
clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed)
jobs.joinAll()
}
}
private suspend fun Raise<Throwable>.getAccount(accountId: AccountId): Account.CryptoPortfolio {
val accountList = singleAccountListSupplier.getSyncOrNull(
params = SingleAccountListProducer.Params(userWalletId = accountId.userWalletId),
) ?: raise(IllegalStateException("No accounts for wallet ${accountId.userWalletId}"))
return accountList.accounts.firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio
?: raise(IllegalStateException("No account with id $accountId"))
}
private fun Set<CryptoCurrency>.modify(
add: List<CryptoCurrency>,
remove: List<CryptoCurrency>,
): ModifiedCurrencyList {
val mutableCurrencies = this.toMutableList()
val added = mutableListOf<CryptoCurrency>()
val removed = mutableListOf<CryptoCurrency>()
val existingCurrenciesById = mutableCurrencies.associateBy(::TempID)
add.groupByNetwork { !existingCurrenciesById.containsKey(it) }
.forEach { (network, currenciesById) ->
val coinTempId = TempID(network)
if (!existingCurrenciesById.containsKey(coinTempId)) {
val coin = currenciesById[coinTempId]
if (coin != null) {
mutableCurrencies.add(coin)
added.add(coin)
currenciesById.remove(coinTempId)
} else {
val createdCoin = currenciesRepository.createCoinCurrency(network)
mutableCurrencies.add(createdCoin)
added.add(createdCoin)
}
}
mutableCurrencies.addAll(currenciesById.values)
added.addAll(currenciesById.values)
}
remove.groupByNetwork(valuePredicate = existingCurrenciesById::containsKey)
.forEach { (network, currenciesById) ->
val coinTempId = TempID(network)
if (currenciesById.containsKey(coinTempId)) {
val existingNetworkCurrenciesCount = mutableCurrencies.count { it.network == network }
if (existingNetworkCurrenciesCount != currenciesById.size) {
return@forEach
}
}
mutableCurrencies.removeAll(currenciesById.values)
removed.addAll(currenciesById.values)
}
return ModifiedCurrencyList(added = added, removed = removed, total = mutableCurrencies)
}
private suspend fun Raise<Throwable>.saveAccount(account: Account.CryptoPortfolio) {
catch(
block = { accountsCRUDRepository.saveAccount(account) },
catch = ::raise,
)
}
private suspend fun Raise<Throwable>.derivePublicKeys(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
) {
catch(
block = { derivationsRepository.derivePublicKeys(userWalletId = userWalletId, currencies = currencies) },
catch = ::raise,
)
}
private fun List<CryptoCurrency>.groupByNetwork(
valuePredicate: (TempID) -> Boolean,
): LinkedHashMap<Network, MutableMap<TempID, CryptoCurrency>> {
val destination = LinkedHashMap<Network, MutableMap<TempID, CryptoCurrency>>()
for (currency in this) {
val key = currency.network
val mutableMap = destination.getOrPut(key) { mutableMapOf() }
val id = TempID(currency)
if (valuePredicate(id)) {
mutableMap.put(id, currency)
}
}
return destination
}
private suspend fun refreshBalances(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<Job> {
if (currencies.isEmpty()) return emptyList()
return coroutineScope {
listOf(
launch { refreshNetworks(userWalletId = userWalletId, currencies = currencies) },
launch { refreshYieldBalances(userWalletId = userWalletId, currencies = currencies) },
launch { refreshQuotes(currencies = currencies) },
)
}
}
private suspend fun refreshNetworks(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network),
),
)
currenciesRepository.syncTokens(userWalletId)
}
private suspend fun refreshYieldBalances(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val stakingIds = currencies.mapNotNullTo(hashSetOf()) {
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
multiYieldBalanceFetcher(
params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
)
}
private suspend fun refreshQuotes(currencies: List<CryptoCurrency>) {
multiQuoteStatusFetcher(
params = MultiQuoteStatusFetcher.Params(
currenciesIds = currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
appCurrencyId = null,
),
)
}
private suspend fun clearMetadata(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<Job> {
if (currencies.isEmpty()) return emptyList()
return coroutineScope {
listOf(
launch { networksCleaner(userWalletId = userWalletId, currencies = currencies) },
launch { clearStaking(userWalletId = userWalletId, currencies = currencies) },
)
}
}
private suspend fun clearStaking(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val stakingIds = currencies.mapNotNullTo(hashSetOf()) {
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
stakingCleaner(userWalletId = userWalletId, stakingIds = stakingIds)
}
private data class TempID(
val networkId: String,
val derivationPath: Network.DerivationPath,
val contractAddress: String?,
) {
constructor(network: Network) : this(
networkId = network.backendId,
derivationPath = network.derivationPath,
contractAddress = null,
)
constructor(currency: CryptoCurrency) : this(
networkId = currency.network.backendId,
derivationPath = currency.network.derivationPath,
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
)
}
private data class ModifiedCurrencyList(
val added: List<CryptoCurrency>,
val removed: List<CryptoCurrency>,
val total: List<CryptoCurrency>,
)
}

View file

@ -57,13 +57,17 @@ sealed interface Account {
val networksCount: Int
get() = cryptoCurrencies.map(CryptoCurrency::network).distinct().size
fun copy(accountName: AccountName = this.accountName, icon: CryptoPortfolioIcon = this.icon): CryptoPortfolio {
fun copy(
accountName: AccountName = this.accountName,
icon: CryptoPortfolioIcon = this.icon,
cryptoCurrencies: Set<CryptoCurrency> = this.cryptoCurrencies,
): CryptoPortfolio {
return CryptoPortfolio(
accountId = this.accountId,
accountName = accountName,
icon = icon,
derivationIndex = this.derivationIndex,
cryptoCurrencies = this.cryptoCurrencies,
cryptoCurrencies = cryptoCurrencies,
)
}

View file

@ -40,26 +40,7 @@ interface CurrenciesRepository {
* @param userWalletId The unique identifier of the user wallet.
* @param currencies The list of cryptocurrencies to be saved.
*/
suspend fun saveNewCurrenciesList(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
/**
* Add currencies to a specific user wallet.
*
* @param userWalletId The unique identifier of the user wallet.
* @param currencies The currencies which must be added.
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<CryptoCurrency>
/**
* Saves the given list of cryptocurrencies for a specific multi-currency user wallet.
*
* @param userWalletId The unique identifier of the user wallet.
* @param currencies The list of cryptocurrencies to be saved.
*/
@Deprecated("Tech debt")
suspend fun saveNewCurrenciesListCache(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
suspend fun saveCurrenciesLocal(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
/**
* Add currencies to a specific user wallet.
@ -256,6 +237,8 @@ interface CurrenciesRepository {
*/
suspend fun getFeePaidCurrency(userWalletId: UserWalletId, network: Network): FeePaidCurrency
fun createCoinCurrency(network: Network): CryptoCurrency.Coin
/**
* Creates token [cryptoCurrency] based on current token and [network] it`s will be added
*/

View file

@ -46,14 +46,7 @@ internal class MockCurrenciesRepository(
isTokensSortedByBalanceAfterSortingApply = isSortedByBalance
}
override suspend fun saveNewCurrenciesList(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
override suspend fun addCurrencies(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): List<CryptoCurrency> = emptyList()
override suspend fun saveNewCurrenciesListCache(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
override suspend fun saveCurrenciesLocal(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
override suspend fun addCurrenciesCache(
userWalletId: UserWalletId,
@ -152,6 +145,10 @@ internal class MockCurrenciesRepository(
return FeePaidCurrency.Coin
}
override fun createCoinCurrency(network: Network): CryptoCurrency.Coin {
error("not implemented")
}
override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token {
return cryptoCurrency
}