Updated on 2026-08-14
This commit is contained in:
parent
5b95c59a92
commit
50806204ab
9 changed files with 357 additions and 92 deletions
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue