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

@ -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>,
)
}