Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-17 14:05:58 +03:00
commit 3cbc2dadfb
822 changed files with 12838 additions and 5366 deletions

View file

@ -13,6 +13,7 @@ dependencies {
api(projects.domain.core)
api(projects.domain.models)
api(projects.domain.wallets.models)
api(projects.domain.yieldSupply.models)
implementation(deps.arrow.core)
implementation(deps.kotlin.coroutines)

View file

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MultilineLambdaItParameter:GetArchivedAccountsUseCase.kt$GetArchivedAccountsUseCase${ send(it.lceError()) return }</ID>
<ID>MultilineLambdaItParameter:GetArchivedAccountsUseCase.kt$GetArchivedAccountsUseCase${ send(it.lceError()) return@channelFlow }</ID>
<ID>NamedArguments:AddCryptoPortfolioUseCase.kt$AddCryptoPortfolioUseCase$createAccount(userWalletId, accountName, icon, derivationIndex)</ID>
<ID>UnnecessaryAbstractClass:MultiAccountListSupplier.kt$MultiAccountListSupplier$MultiAccountListSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleAccountListSupplier.kt$SingleAccountListSupplier$SingleAccountListSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleAccountSupplier.kt$SingleAccountSupplier$SingleAccountSupplier</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -28,6 +28,7 @@ data class AccountList private constructor(
val userWalletId: UserWalletId,
val accounts: List<Account>,
val totalAccounts: Int,
val totalArchivedAccounts: Int,
val sortType: TokensSortType,
val groupType: TokensGroupType,
) {
@ -60,6 +61,7 @@ data class AccountList private constructor(
userWalletId = this.userWalletId,
accounts = accounts,
totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0,
totalArchivedAccounts = this.totalArchivedAccounts,
sortType = this.sortType,
groupType = this.groupType,
)
@ -82,6 +84,7 @@ data class AccountList private constructor(
userWalletId = this.userWalletId,
accounts = accounts,
totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0,
totalArchivedAccounts = this.totalArchivedAccounts,
sortType = this.sortType,
groupType = this.groupType,
)
@ -152,6 +155,7 @@ data class AccountList private constructor(
companion object {
const val MAX_ACCOUNTS_COUNT = 20
const val MAX_ARCHIVED_ACCOUNTS_COUNT = 1000
private const val MAX_MAIN_ACCOUNTS_COUNT = 1
/**
@ -166,6 +170,7 @@ data class AccountList private constructor(
userWalletId: UserWalletId,
accounts: List<Account>,
totalAccounts: Int,
totalArchivedAccounts: Int,
sortType: TokensSortType = TokensSortType.NONE,
groupType: TokensGroupType = TokensGroupType.NONE,
): Either<Error, AccountList> = either {
@ -200,6 +205,7 @@ data class AccountList private constructor(
userWalletId = userWalletId,
accounts = accounts,
totalAccounts = totalAccounts,
totalArchivedAccounts = totalArchivedAccounts,
sortType = sortType,
groupType = groupType,
)
@ -225,6 +231,7 @@ data class AccountList private constructor(
),
),
totalAccounts = 1,
totalArchivedAccounts = 0,
sortType = sortType,
groupType = groupType,
)

View file

@ -26,6 +26,7 @@ data class AccountStatusList(
val userWalletId: UserWalletId,
val accountStatuses: List<AccountStatus>,
val totalAccounts: Int,
val totalArchivedAccounts: Int,
val totalFiatBalance: TotalFiatBalance,
val sortType: TokensSortType,
val groupType: TokensGroupType,
@ -47,6 +48,7 @@ data class AccountStatusList(
userWalletId = userWalletId,
accounts = accountStatuses.map(AccountStatus::account),
totalAccounts = totalAccounts,
totalArchivedAccounts = totalArchivedAccounts,
sortType = sortType,
groupType = groupType,
)

View file

@ -55,10 +55,11 @@ class ApplyAccountListSortingUseCase(
val updatedAccountList = withError(
transform = { Error.DataOperationFailed("Unable to create AccountList: $it") },
) {
AccountList(
AccountList.invoke(
userWalletId = accountList.userWalletId,
accounts = sortedAccounts,
totalAccounts = accountList.totalAccounts,
totalArchivedAccounts = accountList.totalArchivedAccounts,
sortType = accountList.sortType,
groupType = accountList.groupType,
)
@ -69,9 +70,16 @@ class ApplyAccountListSortingUseCase(
return@eitherOn
}
accountsCRUDRepository.saveAccounts(accountList = updatedAccountList)
applySorting(accountList = updatedAccountList)
}
private suspend fun applySorting(accountList: AccountList) {
catch(
block = { accountsCRUDRepository.saveAccounts(accountList) },
catch = { accountsCRUDRepository.saveAccountsLocally(accountList) },
)
}
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
return catch(
block = { accountsCRUDRepository.getAccountListSync(userWalletId = userWalletId) },

View file

@ -87,6 +87,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = accounts,
totalAccounts = accounts.size,
totalArchivedAccounts = 0,
sortType = sortType,
groupType = groupType,
)
@ -96,6 +97,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = accounts,
totalAccounts = accounts.size,
totalArchivedAccounts = 0,
sortType = sortType,
groupType = groupType,
)
@ -110,6 +112,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = model.accounts,
totalAccounts = model.totalAccounts,
totalArchivedAccounts = 0,
)
// Assert
@ -206,12 +209,14 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = listOf(mainAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
).getOrNull()!!,
toAdd = newAccount,
expected = AccountList(
userWalletId = userWalletId,
accounts = listOf(mainAccount, newAccount),
totalAccounts = 2,
totalArchivedAccounts = 0,
),
)
},
@ -226,12 +231,14 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = listOf(mainAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
).getOrNull()!!,
toAdd = newAccount,
expected = AccountList(
userWalletId = userWalletId,
accounts = listOf(newAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
),
)
},
@ -252,6 +259,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = listOf(mainAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
).getOrNull()!!,
toAdd = newAccount,
expected = AccountList.Error.MainAccountNotFound.left(),
@ -268,6 +276,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = listOf(mainAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
).getOrNull()!!,
toAdd = newAccount,
expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(),
@ -284,6 +293,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = listOf(mainAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
).getOrNull()!!,
toAdd = newAccount,
expected = AccountList.Error.DuplicateAccountNames.left(),
@ -296,6 +306,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = createAccounts(count = 20),
totalAccounts = 20,
totalArchivedAccounts = 0,
).getOrNull()!!,
toAdd = createAccount(derivationIndex = 21),
expected = AccountList.Error.ExceedsMaxAccountsCount.left(),
@ -335,12 +346,14 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = listOf(mainAccount, secondaryAccount),
totalAccounts = 2,
totalArchivedAccounts = 0,
).getOrNull()!!,
toRemove = secondaryAccount,
expected = AccountList(
userWalletId = userWalletId,
accounts = listOf(mainAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
),
)
},
@ -354,6 +367,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = listOf(mainAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
)
MinusTestModel(
@ -372,6 +386,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = listOf(mainAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
).getOrNull()!!,
toRemove = mainAccount,
expected = AccountList.Error.EmptyAccountsList.left(),
@ -388,6 +403,7 @@ internal class AccountListTest {
userWalletId = userWalletId,
accounts = listOf(mainAccount, secondaryAccount),
totalAccounts = 2,
totalArchivedAccounts = 0,
).getOrNull()!!,
toRemove = mainAccount,
expected = AccountList.Error.MainAccountNotFound.left(),

View file

@ -157,6 +157,7 @@ class ApplyAccountListSortingUseCaseTest {
userWalletId = accountList.userWalletId,
accounts = accountList.accounts.reversed(),
totalAccounts = accountList.totalAccounts,
totalArchivedAccounts = accountList.totalArchivedAccounts,
sortType = accountList.sortType,
groupType = accountList.groupType,
).getOrNull()!!

View file

@ -23,9 +23,11 @@ dependencies {
api(projects.domain.quotes)
api(projects.domain.models)
api(projects.domain.networks)
api(projects.domain.nft)
api(projects.domain.referral)
api(projects.domain.staking)
api(projects.domain.tokens)
api(projects.domain.walletManager)
api(projects.domain.wallets)
implementation(projects.libs.blockchainSdk)

View file

@ -7,8 +7,6 @@
<ID>MultilineLambdaItParameter:AccountCryptoCurrencyStatusFinder.kt$AccountCryptoCurrencyStatusFinder${ val currency = it.currency val isContractAddressMatch = contractAddress == null || currency.id.contractAddress.equals(contractAddress, ignoreCase = true) currency.network.rawId == networkId.rawId.value &amp;&amp; currency.network.derivationPath.value == derivationPath.value &amp;&amp; isContractAddressMatch }</ID>
<ID>MultilineLambdaItParameter:ApplyTokenListSortingUseCaseV2.kt$ApplyTokenListSortingUseCaseV2${ errors[account.accountId] = it return@map account }</ID>
<ID>MultilineLambdaItParameter:DefaultMultiAccountStatusListProducer.kt$DefaultMultiAccountStatusListProducer${ singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(it.walletId), ) }</ID>
<ID>MultilineLambdaItParameter:ManageCryptoCurrenciesUseCase.kt$ManageCryptoCurrenciesUseCase${ ExpressAsset.ID( networkId = it.network.backendId, contractAddress = (it as? CryptoCurrency.Token)?.contractAddress, ) }</ID>
<ID>MultilineLambdaItParameter:ManageCryptoCurrenciesUseCase.kt$ManageCryptoCurrenciesUseCase${ it.network.backendId == networkId &amp;&amp; !it.isCustom &amp;&amp; it.contractAddress.equals(contractAddress, true) }</ID>
<ID>UnnecessaryAbstractClass:MultiAccountStatusListSupplier.kt$MultiAccountStatusListSupplier$MultiAccountStatusListSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleAccountStatusListSupplier.kt$SingleAccountStatusListSupplier$SingleAccountStatusListSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleAccountStatusSupplier.kt$SingleAccountStatusSupplier$SingleAccountStatusSupplier</ID>

View file

@ -12,10 +12,11 @@ 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.multi.MultiStakingBalanceFetcher
import com.tangem.domain.staking.utils.StakingCleaner
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -95,6 +96,7 @@ internal object AccountStatusUseCaseModule {
accountsCRUDRepository: AccountsCRUDRepository,
currenciesRepository: CurrenciesRepository,
derivationsRepository: DerivationsRepository,
walletManagersFacade: WalletManagersFacade,
cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
stakingIdFactory: StakingIdFactory,
networksCleaner: NetworksCleaner,
@ -107,6 +109,7 @@ internal object AccountStatusUseCaseModule {
accountsCRUDRepository = accountsCRUDRepository,
currenciesRepository = currenciesRepository,
derivationsRepository = derivationsRepository,
walletManagersFacade = walletManagersFacade,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
stakingIdFactory = stakingIdFactory,
networksCleaner = networksCleaner,
@ -120,18 +123,16 @@ internal object AccountStatusUseCaseModule {
@Provides
@Singleton
fun provideCryptoCurrencyBalanceFetcher(
accountsCRUDRepository: AccountsCRUDRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): CryptoCurrencyBalanceFetcher {
return CryptoCurrencyBalanceFetcher(
accountsCRUDRepository = accountsCRUDRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
)

View file

@ -68,6 +68,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
accountStatuses = accountStatuses.toList(),
totalAccounts = accountList.totalAccounts,
totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances),
totalArchivedAccounts = accountList.totalArchivedAccounts,
sortType = accountList.sortType,
groupType = accountList.groupType,
)
@ -194,6 +195,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
}
},
totalAccounts = accountList.totalAccounts,
totalArchivedAccounts = accountList.totalArchivedAccounts,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = accountList.sortType,
groupType = accountList.groupType,

View file

@ -106,6 +106,7 @@ class ApplyTokenListSortingUseCaseV2(
userWalletId = accountList.userWalletId,
accounts = accountList.accounts.sortTokens(sortedTokensIdsByAccount, errors),
totalAccounts = accountList.totalAccounts,
totalArchivedAccounts = accountList.totalArchivedAccounts,
sortType = if (isSortedByBalance) TokensSortType.BALANCE else TokensSortType.NONE,
groupType = if (isGroupedByNetwork) TokensGroupType.NETWORK else TokensGroupType.NONE,
).getOrElse {

View file

@ -21,8 +21,10 @@ import com.tangem.domain.networks.utils.NetworksCleaner
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.utils.StakingCleaner
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.*
import timber.log.Timber
@ -49,6 +51,7 @@ class ManageCryptoCurrenciesUseCase(
private val accountsCRUDRepository: AccountsCRUDRepository,
private val currenciesRepository: CurrenciesRepository,
private val derivationsRepository: DerivationsRepository,
private val walletManagersFacade: WalletManagersFacade,
private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
private val stakingIdFactory: StakingIdFactory,
private val networksCleaner: NetworksCleaner,
@ -83,27 +86,19 @@ class ManageCryptoCurrenciesUseCase(
val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies()
.modify(add = add, remove = remove)
if (!modifiedCurrencyList.hasChanges) {
Timber.d("No changes in currencies, skipping")
return@withContext
}
saveAccount(
account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()),
)
val isDerivingFailed = derivePublicKeys(
userWalletId = userWalletId,
currencies = modifiedCurrencyList.added,
).isLeft()
derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
parallelUpdatingScope.launch {
/*
* If only removal of currencies happened, we need to sync tokens. Otherwise, tokens will be synced
* when balances are refreshed for added currencies.
*/
val isOnlyRemoval = modifiedCurrencyList.added.isEmpty() && modifiedCurrencyList.removed.isNotEmpty()
if (isDerivingFailed || isOnlyRemoval) {
launch { accountsCRUDRepository.syncTokens(userWalletId) }
}
if (isDerivingFailed) return@launch
syncTokens(userWalletId, modifiedCurrencyList)
cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total)
@ -124,10 +119,10 @@ class ManageCryptoCurrenciesUseCase(
val foundToken = accountStatus.tokenList.flattenCurrencies()
.mapNotNull { it.currency as? CryptoCurrency.Token }
.firstOrNull {
it.network.backendId == networkId &&
!it.isCustom &&
it.contractAddress.equals(contractAddress, true)
.firstOrNull { token ->
token.network.backendId == networkId &&
!token.isCustom &&
token.contractAddress.equals(contractAddress, true)
}
if (foundToken != null) return@withContext foundToken
@ -140,6 +135,8 @@ class ManageCryptoCurrenciesUseCase(
saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()))
parallelUpdatingScope.launch {
syncTokens(userWalletId, modifiedCurrencyList)
cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd))
refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total)
}
@ -263,15 +260,40 @@ class ManageCryptoCurrenciesUseCase(
)
}
private suspend fun syncTokens(userWalletId: UserWalletId, modifiedCurrencyList: ModifiedCurrencyList) {
createWalletManagers(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
.onFailure { Timber.e(it, "Failed to sync tokens for wallet $userWalletId") }
}
/**
* Creates wallet managers for the given [currencies] if they do not already exist.
* The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature.
*
* @param userWalletId The ID of the user's wallet.
* @param currencies The list of cryptocurrencies for which to create wallet managers.
*/
private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network)
for (network in networks) {
runSuspendCatching {
walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network)
}
.onFailure { Timber.e(it, "Failed to create wallet manager for network ${network.id}") }
}
}
private suspend fun refreshExpress(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
coroutineScope {
launch {
val assetIds = currencies.mapTo(hashSetOf()) {
val assetIds = currencies.mapTo(hashSetOf()) { currency ->
ExpressAsset.ID(
networkId = it.network.backendId,
contractAddress = (it as? CryptoCurrency.Token)?.contractAddress,
networkId = currency.network.backendId,
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
)
}
@ -328,5 +350,8 @@ class ManageCryptoCurrenciesUseCase(
val added: List<CryptoCurrency>,
val removed: List<CryptoCurrency>,
val total: List<CryptoCurrency>,
)
) {
val hasChanges get() = added.isNotEmpty() || removed.isNotEmpty()
}
}

View file

@ -7,6 +7,7 @@ import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.repository.AccountsCRUDRepository
@ -23,6 +24,8 @@ import com.tangem.domain.models.wallet.UserWalletId
*
* @property crudRepository repository for performing CRUD operations on accounts
* @property mainAccountTokensMigration handles the migration of tokens from the main account to the recovered account
* @property cryptoCurrencyBalanceFetcher Fetcher for updating crypto currency balances.
* @property singleAccountListFetcher fetches the list of accounts for a single user wallet
*
[REDACTED_AUTHOR]
*/
@ -30,6 +33,7 @@ class RecoverCryptoPortfolioUseCase(
private val crudRepository: AccountsCRUDRepository,
private val mainAccountTokensMigration: MainAccountTokensMigration,
private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
private val singleAccountListFetcher: SingleAccountListFetcher,
) {
/**
@ -38,6 +42,8 @@ class RecoverCryptoPortfolioUseCase(
* @param accountId the unique identifier of the account to recover
*/
suspend operator fun invoke(accountId: AccountId): Either<Error, Account.CryptoPortfolio> = either {
fetchAccountList(userWalletId = accountId.userWalletId)
val accountList = getAccountList(userWalletId = accountId.userWalletId)
ensure(accountList.canAddMoreAccounts) {
@ -62,6 +68,12 @@ class RecoverCryptoPortfolioUseCase(
recoveredAccount
}
private suspend fun Raise<Error>.fetchAccountList(userWalletId: UserWalletId) {
singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId)).onLeft {
raise(Error.DataOperationFailed(cause = it))
}
}
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
return catch(
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) },

View file

@ -1,14 +1,12 @@
package com.tangem.domain.account.status.utils
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
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.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.wallet.FetchingSource
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
@ -19,20 +17,18 @@ import timber.log.Timber
* Utility class responsible for fetching and refreshing the balances of various crypto currencies
* associated with a user's wallet.
*
* @property accountsCRUDRepository Repository for managing account data.
* @property multiNetworkStatusFetcher Fetcher for updating network statuses.
* @property multiQuoteStatusFetcher Fetcher for updating quote statuses.
* @property multiYieldBalanceFetcher Fetcher for updating yield balances.
* @property multiStakingBalanceFetcher Fetcher for updating staking balances.
* @property stakingIdFactory Factory for creating staking IDs.
* @property parallelUpdatingScope Coroutine scope for parallel balance updates.
*
[REDACTED_AUTHOR]
*/
class CryptoCurrencyBalanceFetcher(
private val accountsCRUDRepository: AccountsCRUDRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
private val stakingIdFactory: StakingIdFactory,
private val parallelUpdatingScope: CoroutineScope,
) {
@ -56,7 +52,10 @@ class CryptoCurrencyBalanceFetcher(
FetchingSource.NETWORK to refreshNetworks(userWalletId = userWalletId, currencies = currencies)
},
async {
FetchingSource.STAKING to refreshYieldBalances(userWalletId = userWalletId, currencies = currencies)
FetchingSource.STAKING to refreshStakingBalances(
userWalletId = userWalletId,
currencies = currencies,
)
},
async { FetchingSource.QUOTE to refreshQuotes(currencies = currencies) },
)
@ -80,25 +79,16 @@ class CryptoCurrencyBalanceFetcher(
private suspend fun refreshNetworks(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> = either {
val either = multiNetworkStatusFetcher(
): Either<Throwable, Unit> {
return multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network),
),
)
arrow.core.raise.catch(
block = { accountsCRUDRepository.syncTokens(userWalletId) },
catch = {
Timber.e(it, "Failed to sync tokens for wallet: $userWalletId")
},
)
return either
}
private suspend fun refreshYieldBalances(
private suspend fun refreshStakingBalances(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> {
@ -106,8 +96,8 @@ class CryptoCurrencyBalanceFetcher(
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
return multiYieldBalanceFetcher(
params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
return multiStakingBalanceFetcher(
params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
)
}

View file

@ -7,7 +7,7 @@ import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.network.getAddress
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
@ -16,8 +16,8 @@ import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@ -28,7 +28,7 @@ import javax.inject.Inject
*
* @property singleNetworkStatusSupplier Supplier for obtaining network status.
* @property singleQuoteStatusSupplier Supplier for obtaining quote status.
* @property singleYieldBalanceSupplier Supplier for obtaining yield balance.
* @property singleStakingBalanceSupplier Supplier for obtaining staking balance.
* @property stakingIdFactory Factory for creating staking IDs.
*
[REDACTED_AUTHOR]
@ -36,7 +36,7 @@ import javax.inject.Inject
internal class CryptoCurrencyStatusesFlowFactory @Inject constructor(
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier,
private val stakingIdFactory: StakingIdFactory,
) {
@ -53,7 +53,7 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor(
currency = currency,
maybeNetworkStatus = statusSources.networkStatus.toOption(),
maybeQuoteStatus = statusSources.quoteStatus.toOption(),
maybeYieldBalance = statusSources.yieldBalance.toOption(),
maybeStakingBalance = statusSources.stakingBalance.toOption(),
)
}
.onEmpty {
@ -71,10 +71,10 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor(
): Flow<CryptoCurrencyStatusSources> {
val networkStatusFlow = getNetworkStatusFlow(userWalletId = userWallet.walletId, network = currency.network)
val yieldBalanceFlow = if (userWallet.isMultiCurrency) {
val stakingBalanceFlow = if (userWallet.isMultiCurrency) {
networkStatusFlow.flatMapLatest { networkStatus ->
if (networkStatus != null) {
getYieldBalanceFlow(
getStakingBalanceFlow(
userWalletId = userWallet.walletId,
currencyId = currency.id,
networkStatus = networkStatus,
@ -89,26 +89,26 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor(
val quoteStatusFlow = currency.id.rawCurrencyId?.let(::getQuoteStatusFlow)
return combine(networkStatusFlow, yieldBalanceFlow, quoteStatusFlow)
return combine(networkStatusFlow, stakingBalanceFlow, quoteStatusFlow)
.distinctUntilChanged()
}
private fun combine(
networkStatusFlow: Flow<NetworkStatus?>,
yieldBalanceFlow: Flow<YieldBalance?>?,
stakingBalanceFlow: Flow<StakingBalance?>?,
quoteStatusFlow: Flow<QuoteStatus>?,
): Flow<CryptoCurrencyStatusSources> {
return when {
yieldBalanceFlow != null && quoteStatusFlow != null -> {
stakingBalanceFlow != null && quoteStatusFlow != null -> {
combine(
flow = networkStatusFlow,
flow2 = yieldBalanceFlow,
flow2 = stakingBalanceFlow,
flow3 = quoteStatusFlow,
transform = ::CryptoCurrencyStatusSources,
)
}
yieldBalanceFlow != null -> {
combine(flow = networkStatusFlow, flow2 = yieldBalanceFlow, transform = ::CryptoCurrencyStatusSources)
stakingBalanceFlow != null -> {
combine(flow = networkStatusFlow, flow2 = stakingBalanceFlow, transform = ::CryptoCurrencyStatusSources)
}
quoteStatusFlow != null -> {
combine(flow = networkStatusFlow, flow2 = quoteStatusFlow) { networkStatus, quoteStatus ->
@ -136,11 +136,11 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor(
.distinctUntilChanged()
}
private fun getYieldBalanceFlow(
private fun getStakingBalanceFlow(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
networkStatus: NetworkStatus,
): Flow<YieldBalance?> {
): Flow<StakingBalance?> {
val stakingId = stakingIdFactory.create(
currencyId = currencyId,
defaultAddress = networkStatus.getAddress(),
@ -148,8 +148,8 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor(
.getOrNull()
return if (stakingId != null) {
singleYieldBalanceSupplier(
params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId),
singleStakingBalanceSupplier(
params = SingleStakingBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId),
)
.distinctUntilChanged()
} else {
@ -159,7 +159,7 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor(
private data class CryptoCurrencyStatusSources(
val networkStatus: NetworkStatus? = null,
val yieldBalance: YieldBalance? = null,
val stakingBalance: StakingBalance? = null,
val quoteStatus: QuoteStatus? = null,
)
}

View file

@ -90,6 +90,7 @@ class DefaultSingleAccountStatusListProducerTest {
),
),
totalAccounts = 1,
totalArchivedAccounts = accountList.totalArchivedAccounts,
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
sortType = accountList.sortType,
groupType = accountList.groupType,
@ -132,6 +133,7 @@ class DefaultSingleAccountStatusListProducerTest {
),
),
totalAccounts = 1,
totalArchivedAccounts = accountList.totalArchivedAccounts,
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
sortType = accountList.sortType,
groupType = accountList.groupType,
@ -153,6 +155,7 @@ class DefaultSingleAccountStatusListProducerTest {
),
),
totalAccounts = 1,
totalArchivedAccounts = accountList.totalArchivedAccounts,
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
sortType = updatedAccountList.sortType,
groupType = updatedAccountList.groupType,
@ -192,6 +195,7 @@ class DefaultSingleAccountStatusListProducerTest {
),
),
totalAccounts = 1,
totalArchivedAccounts = accountList.totalArchivedAccounts,
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
sortType = accountList.sortType,
groupType = accountList.groupType,
@ -273,6 +277,7 @@ class DefaultSingleAccountStatusListProducerTest {
),
),
totalAccounts = 1,
totalArchivedAccounts = accountList.totalArchivedAccounts,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = accountList.sortType,
groupType = accountList.groupType,

View file

@ -348,6 +348,7 @@ internal class ApplyTokenListSortingUseCaseTest {
customAccount, // unchanged due to error
),
totalAccounts = accountList.totalAccounts,
totalArchivedAccounts = accountList.totalArchivedAccounts,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)

View file

@ -295,6 +295,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
)
},
totalAccounts = totalAccounts,
totalArchivedAccounts = totalArchivedAccounts,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = sortType,
groupType = groupType,

View file

@ -5,6 +5,7 @@ import arrow.core.left
import arrow.core.right
import arrow.core.toOption
import com.google.common.truth.Truth
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.repository.AccountsCRUDRepository
@ -27,17 +28,19 @@ import kotlin.random.Random
class RecoverCryptoPortfolioUseCaseTest {
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
private val singleAccountListFetcher: SingleAccountListFetcher = mockk()
private val mainAccountTokensMigration: MainAccountTokensMigration = mockk()
private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher = mockk(relaxUnitFun = true)
private val useCase = RecoverCryptoPortfolioUseCase(
crudRepository = crudRepository,
mainAccountTokensMigration = mainAccountTokensMigration,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
singleAccountListFetcher = singleAccountListFetcher,
)
@BeforeEach
fun resetMocks() {
clearMocks(crudRepository, mainAccountTokensMigration, cryptoCurrencyBalanceFetcher)
clearMocks(crudRepository, mainAccountTokensMigration, cryptoCurrencyBalanceFetcher, singleAccountListFetcher)
}
@Test
@ -56,6 +59,7 @@ class RecoverCryptoPortfolioUseCaseTest {
val updatedAccountList = (accountList + account).getOrNull()!!
coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption()
coEvery { mainAccountTokensMigration.migrate(userWalletId, account.derivationIndex) } returns Unit.right()
@ -69,6 +73,7 @@ class RecoverCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifySequence {
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId)
crudRepository.getArchivedAccountSync(account.accountId)
crudRepository.saveAccounts(updatedAccountList)
@ -85,6 +90,7 @@ class RecoverCryptoPortfolioUseCaseTest {
derivationIndex = DerivationIndex.Companion.Main,
)
coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns None
// Act
@ -95,7 +101,8 @@ class RecoverCryptoPortfolioUseCaseTest {
Truth.assertThat(actual.cause).isInstanceOf(expected::class.java)
Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message)
coVerifySequence { crudRepository.getAccountListSync(userWalletId) }
coVerifySequence { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) {
crudRepository.getArchivedAccountSync(any())
crudRepository.saveAccounts(any())
@ -111,6 +118,7 @@ class RecoverCryptoPortfolioUseCaseTest {
)
val exception = IllegalStateException("Test error")
coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right()
coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception
// Act
@ -120,7 +128,8 @@ class RecoverCryptoPortfolioUseCaseTest {
val expected = DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifySequence { crudRepository.getAccountListSync(userWalletId) }
coVerifySequence { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId) }
coVerify(inverse = true) {
crudRepository.getArchivedAccountSync(any())
crudRepository.saveAccounts(any())
@ -134,6 +143,7 @@ class RecoverCryptoPortfolioUseCaseTest {
val accountList = AccountList.Companion.empty(userWalletId)
val exception = IllegalStateException("Test error")
coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } throws exception
@ -145,6 +155,7 @@ class RecoverCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifySequence {
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId)
crudRepository.getArchivedAccountSync(account.accountId)
}
@ -157,6 +168,7 @@ class RecoverCryptoPortfolioUseCaseTest {
val account = createAccount(userWalletId)
val accountList = AccountList.Companion.empty(userWalletId)
coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None
@ -169,6 +181,7 @@ class RecoverCryptoPortfolioUseCaseTest {
Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message)
coVerifySequence {
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId)
crudRepository.getArchivedAccountSync(account.accountId)
}
@ -192,6 +205,7 @@ class RecoverCryptoPortfolioUseCaseTest {
val updatedAccountList = (accountList + account).getOrNull()!!
val exception = IllegalStateException("Save failed")
coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right()
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption()
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
@ -204,12 +218,41 @@ class RecoverCryptoPortfolioUseCaseTest {
Truth.assertThat(actual).isEqualTo(expected)
coVerifySequence {
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId)
crudRepository.getArchivedAccountSync(account.accountId)
crudRepository.saveAccounts(updatedAccountList)
}
}
@Test
fun `invoke should return error if fetch is failed`() = runTest {
// Arrange
val accountId = AccountId.Companion.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = DerivationIndex.Companion.Main,
)
val exception = IllegalStateException("Test error")
coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns exception.left()
// Act
val actual = useCase(accountId)
// Assert
val expected = DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifySequence {
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
}
coVerify(inverse = true) {
crudRepository.getAccountListSync(any())
crudRepository.getArchivedAccountSync(any())
crudRepository.saveAccounts(any())
}
}
private fun createAccount(
userWalletId: UserWalletId,
name: String = "Test Account",

View file

@ -42,6 +42,7 @@ class ToggleTokenListGroupingUseCaseV2Test {
userWalletId = userWalletId,
accountStatuses = emptyList(),
totalAccounts = 0,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Failed,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
@ -205,6 +206,7 @@ class ToggleTokenListGroupingUseCaseV2Test {
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = tokenList.totalFiatBalance,
sortType = tokenList.sortedBy,
groupType = groupType,

View file

@ -42,6 +42,7 @@ class ToggleTokenListSortingUseCaseV2Test {
userWalletId = userWalletId,
accountStatuses = emptyList(),
totalAccounts = 0,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Failed,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
@ -145,6 +146,7 @@ class ToggleTokenListSortingUseCaseV2Test {
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = tokenList.totalFiatBalance,
sortType = tokenList.sortedBy,
groupType = TokensGroupType.NONE,

View file

@ -11,7 +11,7 @@ import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
@ -20,8 +20,8 @@ import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
import com.tangem.test.core.getEmittedValues
import io.mockk.*
import kotlinx.coroutines.flow.emptyFlow
@ -41,13 +41,13 @@ class CryptoCurrencyStatusesFlowFactoryTest {
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier = mockk()
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk()
private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier = mockk()
private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier = mockk()
private val stakingIdFactory: StakingIdFactory = mockk()
private val factory = CryptoCurrencyStatusesFlowFactory(
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
singleStakingBalanceSupplier = singleStakingBalanceSupplier,
stakingIdFactory = stakingIdFactory,
)
@ -63,7 +63,7 @@ class CryptoCurrencyStatusesFlowFactoryTest {
clearMocks(
singleNetworkStatusSupplier,
singleQuoteStatusSupplier,
singleYieldBalanceSupplier,
singleStakingBalanceSupplier,
stakingIdFactory,
)
}
@ -96,13 +96,13 @@ class CryptoCurrencyStatusesFlowFactoryTest {
stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value)
} returns stakingId.right()
val yieldBalance = YieldBalance.Empty(stakingId = stakingId, source = StatusSource.ACTUAL)
val yieldBalanceFlow = flowOf(yieldBalance)
val stakingBalance = StakingBalance.Empty(stakingId = stakingId, source = StatusSource.ACTUAL)
val stakingBalanceFlow = flowOf(stakingBalance)
every {
singleYieldBalanceSupplier(
params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId),
singleStakingBalanceSupplier(
params = SingleStakingBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId),
)
} returns yieldBalanceFlow
} returns stakingBalanceFlow
// Act
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
@ -121,7 +121,7 @@ class CryptoCurrencyStatusesFlowFactoryTest {
coVerify(ordering = Ordering.SEQUENCE) {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value)
singleYieldBalanceSupplier(params = SingleYieldBalanceProducer.Params(userWalletId, stakingId))
singleStakingBalanceSupplier(params = SingleStakingBalanceProducer.Params(userWalletId, stakingId))
}
}

View file

@ -44,7 +44,7 @@ internal fun createStatus(currency: CryptoCurrency, fiatAmount: BigDecimal): Cry
fiatRate = BigDecimal.ONE,
fiatAmount = fiatAmount,
priceChange = BigDecimal.ZERO,
yieldBalance = null,
stakingBalance = null,
hasCurrentNetworkTransactions = false,
yieldSupplyStatus = null,
pendingTransactions = emptySet(),

View file

@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MultilineLambdaItParameter:ListenToFlipsUseCase.kt$ListenToFlipsUseCase${ send(HideBalancesError.DataError(it).left()) return@collectLatest }</ID>
<ID>NoNameShadowing:ListenToFlipsUseCase.kt$ListenToFlipsUseCase${ send(HideBalancesError.DataError(it).left()) return@collectLatest }</ID>
<ID>NoNameShadowing:ListenToFlipsUseCase.kt$ListenToFlipsUseCase${ send(HideBalancesError.DataError(it).left()) }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -1,14 +1,26 @@
package com.tangem.domain.card.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
sealed class IntroductionProcess(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent("Introduction Process", event, params) {
object ScreenOpened : IntroductionProcess("Introduction Process Screen Opened")
object ButtonTokensList : IntroductionProcess("Button - Tokens List")
object ButtonBuyCards : IntroductionProcess("Button - Buy Cards")
object ButtonScanCard : IntroductionProcess("Button - Scan Card")
class ScreenOpened : IntroductionProcess("Introduction Process Screen Opened")
class ButtonTokensList : IntroductionProcess("Button - Tokens List")
class ButtonBuyCards : IntroductionProcess("Button - Buy Cards")
class ButtonScanCardLegacy : IntroductionProcess("Button - Scan Card")
class CreateWalletIntroScreenOpened : IntroductionProcess("Create Wallet Intro Screen Opened")
class ButtonScanCard(
val source: AnalyticsParam.ScreensSources,
) : IntroductionProcess(
event = "Button - Scan Card",
params = mapOf(
AnalyticsParam.Key.SOURCE to source.value,
),
)
}

View file

@ -7,5 +7,5 @@ sealed class Shop(
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent("Shop", event, params) {
object ScreenOpened : Shop("Shop Screen Opened")
class ScreenOpened : Shop("Shop Screen Opened")
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>IgnoredReturnValue:FlowCachingSupplier.kt$FlowCachingSupplier$put(key = key, value = flow)</ID>
<ID>IgnoredReturnValue:FlowCachingSupplier.kt$FlowCachingSupplier$remove(key)</ID>
<ID>MultilineLambdaItParameter:FlowCachingSupplier.kt$FlowCachingSupplier${ it.toMutableMap().apply { put(key = key, value = flow) } }</ID>
<ID>MultilineLambdaItParameter:FlowCachingSupplier.kt$FlowCachingSupplier${ it.toMutableMap().apply { remove(key) } }</ID>
<ID>ObjectExtendsThrowable:DataError.kt$DataError.NetworkError$NoInternetConnection : NetworkError</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,397 @@
package com.tangem.domain.demo.models
@Suppress("LargeClass")
internal object DemoConfigCardIds {
val releaseDemoCardIds = mutableListOf(
// === Not from the Google Sheet table ===
"AC01000000041225",
"AC01000000041472",
"AB01000000046498",
"AB01000000049608",
"AB01000000049574",
"AB01000000046704",
"AB02000000051000",
"AB02000000050911",
// === Mvideo ===
// Wallet
"AC01000000045754",
"AC01000000041662",
"AC01000000041647",
"AC01000000041209",
"AC01000000042462",
"AC01000000041100",
"AC01000000041621",
"AC01000000045960",
"AC01000000041092",
"AC01000000041217",
"AC01000000013489",
"AC01000000028610",
"AC01000000028701",
"AC01000000028578",
"AC01000000027281",
"AC01000000027216",
"AC01000000028594",
"AC01000000028602",
"AC01000000028636",
"AC01000000013968",
"AC01000000027208",
"AC01000000013471",
"AC01000000028586",
"AC01000000013703",
"AC01000000028628",
"AC01000000028693",
"AC01000000028685",
"AC01000000013950",
"AC01000000013828",
"AC01000000013497",
"AC01000000013836",
"AC01000000013505",
"AC03000000046693",
"AC03000000046685",
"AC03000000046677",
"AC03000000046669",
"AC03000000046651",
"AC03000000046644",
"AC03000000046636",
"AC03000000046628",
"AC03000000046610",
"AC03000000046602",
"AC03000000046594",
"AC03000000046586",
"AC03000000046578",
"AC03000000046560",
"AC03000000046552",
"AC03000000046545",
"AC03000000046537",
"AC03000000046529",
"AC03000000046511",
"AC03000000046800",
"AC03000000046792",
"AC03000000046784",
"AC03000000046776",
"AC03000000046768",
"AC03000000046750",
"AC03000000046743",
"AC03000000046735",
"AC03000000046727",
"AC03000000046446",
"AC03000000046438",
"AC03000000046412",
"AC03000000046388",
"AC03000000046370",
"AC03000000046354",
"AC03000000046347",
"AC03000000046339",
"AC03000000046321",
"AC03000000046172",
"AC03000000046396",
"AC03000000046404",
"AC03000000046701",
"AC03000000046420",
"AC03000000046719",
"AC03000000046503",
"AC03000000046495",
"AC03000000046487",
"AC03000000046362",
"AC03000000046479",
"AC03000000046461",
"AC03000000046453",
// Note BTC
"AB01000000059608",
"AB01000000046647",
"AB01000000046571",
"AB01000000046746",
"AB01000000059574",
"AB01000000046753",
"AB01000000046605",
"AB01000000046761",
"AB01000000046720",
"AB01000000046530",
"AB01000000016475",
"AB01000000016483",
"AB01000000016491",
"AB01000000020709",
"AB01000000020717",
"AB01000000015550",
"AB01000000015394",
"AB01000000016079",
"AB01000000016087",
"AB01000000016095",
"AB01000000020915",
"AB01000000017184",
"AB01000000020907",
"AB01000000017192",
"AB01000000016210",
"AB01000000016111",
"AB01000000016103",
"AB01000000015766",
"AB01000000015774",
"AB01000000015782",
"AB01000000022598",
"AB01000000022580",
"AB01000000005688",
"AB07000000005696",
"AB07000000005902",
"AB07000000005910",
"AB07000000005928",
"AB07000000005936",
"AB07000000005944",
"AB07000000005993",
"AB07000000005985",
"AB07000000005977",
"AB07000000005969",
"AB07000000005951",
"AB07000000005605",
"AB07000000005803",
"AB07000000005811",
"AB07000000005829",
"AB07000000005837",
"AB07000000005845",
"AB07000000005852",
"AB07000000005860",
"AB07000000005878",
"AB07000000005886",
"AB07000000005894",
"AB07000000005704",
"AB07000000005712",
"AB07000000005720",
"AB07000000005738",
"AB07000000005746",
"AB07000000005514",
"AB07000000005522",
"AB07000000005563",
"AB07000000005571",
"AB07000000005589",
"AB07000000005597",
"AB07000000005613",
"AB07000000005621",
"AB07000000005639",
"AB07000000005647",
"AB07000000005654",
"AB07000000005662",
"AB07000000005670",
"AB07000000005530",
"AB07000000005548",
"AB07000000005555",
"AB07000000005753",
"AB07000000005761",
"AB07000000005779",
"AB07000000005787",
"AB07000000005795",
"AB07000000005506",
// Note ETH
"AB02000000051083",
"AB02000000051059",
"AB02000000051158",
"AB02000000050986",
"AB02000000051026",
"AB02000000050960",
"AB02000000051042",
"AB02000000051091",
"AB02000000051034",
"AB02000000051133",
"AB02000000019924",
"AB02000000019932",
"AB02000000022092",
"AB02000000022282",
"AB02000000023983",
"AB02000000023439",
"AB02000000020328",
"AB02000000020310",
"AB02000000021565",
"AB02000000022357",
"AB02000000023355",
"AB02000000022324",
"AB02000000022100",
"AB02000000019999",
"AB02000000020013",
"AB02000000020005",
"AB02000000020021",
"AB02000000020039",
"AB02000000020278",
"AB02000000020252",
"AB02000000018652",
"AB02000000018561",
"AB08000000009481",
"AB08000000009473",
"AB08000000009705",
"AB08000000009897",
"AB08000000009689",
"AB08000000009671",
"AB08000000009465",
"AB08000000009457",
"AB08000000009440",
"AB08000000009432",
"AB08000000009424",
"AB08000000009416",
"AB08000000009408",
"AB08000000009390",
"AB08000000009374",
"AB08000000009382",
"AB08000000009267",
"AB08000000009275",
"AB08000000009283",
"AB08000000009291",
"AB08000000009309",
"AB08000000009317",
"AB08000000009325",
"AB08000000009333",
"AB08000000009341",
"AB08000000009358",
"AB08000000009366",
"AB08000000009077",
"AB08000000009143",
"AB08000000009168",
"AB08000000009184",
"AB08000000009192",
"AB08000000009200",
"AB08000000009226",
"AB08000000009218",
"AB08000000009234",
"AB08000000009242",
"AB08000000008574",
"AB08000000009069",
"AB08000000008525",
"AB08000000009051",
"AB08000000009135",
"AB08000000009150",
"AB08000000009176",
"AB08000000009085",
"AB08000000009093",
"AB08000000009101",
"AB08000000009119",
"AB08000000009127",
"AB08000000009259",
// === Technopark ===
// Wallet
"AC01000000044120",
"AC01000000044997",
"AC01000000044989",
"AC01000000043494",
"AC01000000043486",
"AC01000000044187",
"AC01000000043148",
"AC01000000044013",
"AC01000000043973",
"AC01000000044815",
"AC01000000044807",
"AC01000000043809",
"AC01000000043833",
"AC01000000043460",
"AC01000000043064",
"AC01000000044138",
"AC01000000044500",
"AC01000000044492",
"AC01000000044260",
"AC01000000044278",
// Note BTC
"AB01000000049864",
"AB01000000053239",
"AB01000000053056",
"AB01000000054237",
"AB01000000054245",
"AB01000000054211",
"AB01000000054229",
"AB01000000053189",
"AB01000000054195",
"AB01000000050797",
"AB01000000053833",
"AB01000000052124",
"AB01000000051605",
"AB01000000052223",
"AB01000000052207",
"AB01000000052199",
"AB01000000047785",
"AB01000000047850",
"AB01000000047868",
"AB01000000048288",
// Note ETH
"AB02000000049715",
"AB02000000049848",
"AB02000000049814",
"AB02000000049863",
"AB02000000049871",
"AB02000000049855",
"AB02000000049285",
"AB02000000049277",
"AB02000000049558",
"AB02000000049889",
"AB02000000049988",
"AB02000000049707",
"AB02000000049699",
"AB02000000049897",
"AB02000000049905",
"AB02000000049913",
"AB02000000049251",
"AB02000000049533",
"AB02000000049541",
"AB02000000049830",
// === more cids ===
"AC03000000091418",
"AC03000000091400",
"AC03000000099007",
"AC03000000098991",
"AC03000000098942",
"AC03000000091715",
"AC03000000091301",
"AC03000000091343",
"AB01000000055705",
"AB01000000052918",
"AB01000000047710",
"AB01000000052306",
"AB01000000047645",
"AB01000000048957",
"AB01000000052900",
"AB01000000050391",
"AB01000000047363",
"AB02000000053998",
"AB02000000019809",
"AB02000000020872",
"AB02000000022027",
"AB02000000058955",
"AB02000000053253",
"AB02000000048063",
"AB02000000023736",
"AB02000000058187",
"AB02000000000007",
"AC03000000076229",
"AF04000000000118",
// Wallet 2
"AF04000000012006",
"AF04000000012014",
"AF04000000012022",
"AF04000000012030",
"AF15000000257889",
"AF15000000257897",
"AF15000000637809",
"AF15000000640282",
"AF15000001187424",
"AF15000001187408",
"AF15000001187416",
"AF15000001195781",
"AF15000001195773",
"AF15000001195799",
// Wallet 2 QA
"AF12345678912346",
"AF12345678912361",
"AF10100000000076",
"AF10100000000084",
)
val testDemoCardIds = listOf(
"FB20000000000186", // Note ETH
"FB10000000000196", // Note BTC
"FB30000000000176", // Wallet
"FB04000000000152", // Wallet 2
)
val debugTestDemoCardIds = emptyList<String>()
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ObjectExtendsThrowable:ExpressError.kt$ExpressError$UnknownError : ExpressError</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:WalletMetaInfo.kt$WalletMetaInfo$val hotWalletIsBackedUp: Boolean? = null</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,13 @@
package com.tangem.domain.hotwallet
class IsAccessCodeSimpleUseCase {
operator fun invoke(accessCode: String): Boolean {
return isSequential(accessCode) || isRepeatedCharacters(accessCode)
}
fun isSequential(code: String): Boolean = code.length > 1 &&
(code.zipWithNext().all { it.second == it.first + 1 } ||
code.zipWithNext().all { it.second == it.first - 1 })
fun isRepeatedCharacters(code: String): Boolean = code.isNotEmpty() && code.all { it == code.first() }
}

View file

@ -0,0 +1,9 @@
package com.tangem.domain.hotwallet
import com.tangem.domain.hotwallet.repository.HotWalletRepository
class IsHotWalletCreationSupported(private val hotWalletRepository: HotWalletRepository) {
operator fun invoke(): Boolean = hotWalletRepository.isWalletCreationSupported()
fun getLeastVersionName(): String = hotWalletRepository.getLeastSupportedAndroidVersionName()
}

View file

@ -5,6 +5,10 @@ import kotlinx.coroutines.flow.Flow
interface HotWalletRepository {
fun isWalletCreationSupported(): Boolean
fun getLeastSupportedAndroidVersionName(): String
fun accessCodeSkipped(userWalletId: UserWalletId): Flow<Boolean>
suspend fun setAccessCodeSkipped(userWalletId: UserWalletId, skipped: Boolean)

View file

@ -1,11 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:LogConfig.kt$AnalyticsHandlersLogConfig$val amplitude: Boolean = BuildConfig.LOG_ENABLED</ID>
<ID>BooleanPropertyNaming:LogConfig.kt$AnalyticsHandlersLogConfig$val appsflyer: Boolean = BuildConfig.LOG_ENABLED</ID>
<ID>BooleanPropertyNaming:LogConfig.kt$AnalyticsHandlersLogConfig$val firebase: Boolean = BuildConfig.LOG_ENABLED</ID>
<ID>BooleanPropertyNaming:LogConfig.kt$LogConfig$val storeAction: Boolean = BuildConfig.LOG_ENABLED</ID>
<ID>BooleanPropertyNaming:LogConfig.kt$NetworkLogConfig$val blockchainSdkNetwork: Boolean = BuildConfig.LOG_ENABLED</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -4,17 +4,17 @@ import com.tangem.domain.features.BuildConfig
object LogConfig {
const val imageLoader: Boolean = false
val storeAction: Boolean = BuildConfig.LOG_ENABLED
val shouldStoreAction: Boolean = BuildConfig.LOG_ENABLED
val network: NetworkLogConfig = NetworkLogConfig
val analyticsHandlers: AnalyticsHandlersLogConfig = AnalyticsHandlersLogConfig
}
object NetworkLogConfig {
val blockchainSdkNetwork: Boolean = BuildConfig.LOG_ENABLED
val isBlockchainSdkNetworkLogEnabled: Boolean = BuildConfig.LOG_ENABLED
}
object AnalyticsHandlersLogConfig {
val firebase: Boolean = BuildConfig.LOG_ENABLED
val amplitude: Boolean = BuildConfig.LOG_ENABLED
val appsflyer: Boolean = BuildConfig.LOG_ENABLED
val isFirebaseLogEnabled: Boolean = BuildConfig.LOG_ENABLED
val isAmplitudeLogEnabled: Boolean = BuildConfig.LOG_ENABLED
val isAppsflyerLogEnabled: Boolean = BuildConfig.LOG_ENABLED
}

View file

@ -24,6 +24,8 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.legacy)
implementation(tangemDeps.blockchain)
/* Core */
api(projects.core.pagination)
testImplementation(projects.core.pagination)

View file

@ -1,9 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>NamedArguments:CheckIsCurrencyNotAddedUseCase.kt$CheckIsCurrencyNotAddedUseCase$isCurrencyNotAdded(userWalletId, networkId, derivationPath, contractAddress)</ID>
<ID>NamedArguments:CreateCryptoCurrencyUseCase.kt$CreateCryptoCurrencyUseCase$createCustomToken(userWalletId, networkId, derivationPath, formValues)</ID>
<ID>NamedArguments:ValidateTokenFormUseCase.kt$ValidateTokenFormUseCase$zipOrAccumulate( { ensureIsContractAddressValid(formValues.contractAddress, networkId) }, { ensureIsDecimalsValid(formValues.decimals) }, { ensure(formValues.name.isNotBlank()) { CustomTokenFormValidationException.EmptyName } }, { ensure(formValues.symbol.isNotBlank()) { CustomTokenFormValidationException.EmptySymbol } }, ) { contractAddress, decimals, _, _ -&gt; AddCustomTokenForm.Validated.All( contractAddress = contractAddress, symbol = formValues.symbol, name = formValues.name, decimals = decimals, ) }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -15,6 +15,11 @@ class CheckIsCurrencyNotAddedUseCase(
derivationPath: Network.DerivationPath,
contractAddress: String?,
): Either<Throwable, Boolean> = Either.catch {
repository.isCurrencyNotAdded(userWalletId, networkId, derivationPath, contractAddress)
repository.isCurrencyNotAdded(
userWalletId = userWalletId,
networkId = networkId,
derivationPath = derivationPath,
contractAddress = contractAddress,
)
}
}

View file

@ -57,9 +57,18 @@ class CreateCryptoCurrencyUseCase(
formValues: AddCustomTokenForm.Validated.All?,
): Either<Throwable, CryptoCurrency> = Either.catch {
if (formValues == null) {
customTokensRepository.createCoin(userWalletId, networkId, derivationPath)
customTokensRepository.createCoin(
userWalletId = userWalletId,
networkId = networkId,
derivationPath = derivationPath,
)
} else {
customTokensRepository.createCustomToken(userWalletId, networkId, derivationPath, formValues)
customTokensRepository.createCustomToken(
userWalletId = userWalletId,
networkId = networkId,
derivationPath = derivationPath,
formValues = formValues,
)
}
}
}

View file

@ -10,7 +10,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
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.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
@ -28,7 +28,7 @@ class SaveManagedTokensUseCase(
private val derivationsRepository: DerivationsRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
private val stakingIdFactory: StakingIdFactory,
private val parallelUpdatingScope: CoroutineScope,
) {
@ -61,6 +61,8 @@ class SaveManagedTokensUseCase(
parallelUpdatingScope.launch {
withContext(NonCancellable) {
syncTokens(userWalletId = userWalletId, addedCurrencies = savedCurrencies)
launch {
refreshUpdatedNetworks(
userWalletId = userWalletId,
@ -68,7 +70,7 @@ class SaveManagedTokensUseCase(
)
}
launch {
refreshUpdatedYieldBalances(
refreshUpdatedStakingBalances(
userWalletId = userWalletId,
addedCurrencies = savedCurrencies,
)
@ -96,6 +98,26 @@ class SaveManagedTokensUseCase(
)
}
private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies)
currenciesRepository.syncTokens(userWalletId)
}
/**
* Creates wallet managers for the given [currencies] if they do not already exist.
* The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature.
*
* @param userWalletId The ID of the user's wallet.
* @param currencies The list of cryptocurrencies for which to create wallet managers.
*/
private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network)
for (network in networks) {
walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network)
}
}
private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
@ -103,11 +125,9 @@ class SaveManagedTokensUseCase(
networks = addedCurrencies.map(CryptoCurrency::network).toSet(),
),
)
currenciesRepository.syncTokens(userWalletId)
}
private suspend fun refreshUpdatedYieldBalances(
private suspend fun refreshUpdatedStakingBalances(
userWalletId: UserWalletId,
addedCurrencies: List<CryptoCurrency>,
) {
@ -115,8 +135,8 @@ class SaveManagedTokensUseCase(
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
multiYieldBalanceFetcher(
params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
multiStakingBalanceFetcher(
params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
)
}

View file

@ -12,6 +12,7 @@ class ValidateTokenFormUseCase(
private val repository: CustomTokensRepository,
) {
@Suppress("NamedArguments")
suspend operator fun invoke(
networkId: Network.ID,
formValues: AddCustomTokenForm.Raw,

View file

@ -21,6 +21,7 @@ dependencies {
api(projects.domain.networks)
api(projects.domain.staking)
api(projects.domain.quotes)
api(projects.domain.walletManager)
api(projects.domain.wallets)
api(projects.domain.wallets.models)
api(projects.domain.promo)

View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:TokenMarketInfo.kt$TokenMarketInfo.Network$val exchangeable: Boolean</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,23 @@
package com.tangem.domain.markets
import com.tangem.domain.markets.repositories.MarketsTokenRepository
class GetTopFiveMarketTokenUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
) {
operator fun invoke(
batchingContext: TokenListBatchingContext,
order: TokenMarketListConfig.Order,
): TokenListBatchFlow {
return marketsTokenRepository.getTokenListFlow(
batchingContext = batchingContext,
firstBatchSize = DEFAULT_BATCH_SIZE,
nextBatchSize = NEXT_BATCH_SIZE,
)
}
companion object {
private const val DEFAULT_BATCH_SIZE = 5
private const val NEXT_BATCH_SIZE = 0
}
}

View file

@ -9,8 +9,9 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
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.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
@ -31,10 +32,11 @@ import kotlinx.coroutines.withContext
class SaveMarketTokensUseCase(
private val derivationsRepository: DerivationsRepository,
private val marketsTokenRepository: MarketsTokenRepository,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
private val stakingIdFactory: StakingIdFactory,
private val parallelUpdatingScope: CoroutineScope,
) {
@ -80,14 +82,36 @@ class SaveMarketTokensUseCase(
parallelUpdatingScope.launch {
withContext(NonCancellable) {
syncTokens(userWalletId, savedCurrencies)
launch { refreshUpdatedNetworks(userWalletId, savedCurrencies) }
launch { refreshUpdatedYieldBalances(userWalletId, savedCurrencies) }
launch { refreshUpdatedStakingBalances(userWalletId, savedCurrencies) }
launch { refreshUpdatedQuotes(savedCurrencies) }
}
}
}
}
private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies)
currenciesRepository.syncTokens(userWalletId)
}
/**
* Creates wallet managers for the given [currencies] if they do not already exist.
* The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature.
*
* @param userWalletId The ID of the user's wallet.
* @param currencies The list of cryptocurrencies for which to create wallet managers.
*/
private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network)
for (network in networks) {
walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network)
}
}
private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
@ -95,10 +119,9 @@ class SaveMarketTokensUseCase(
networks = addedCurrencies.map(CryptoCurrency::network).toSet(),
),
)
currenciesRepository.syncTokens(userWalletId)
}
private suspend fun refreshUpdatedYieldBalances(
private suspend fun refreshUpdatedStakingBalances(
userWalletId: UserWalletId,
existingCurrencies: List<CryptoCurrency>,
) {
@ -106,8 +129,8 @@ class SaveMarketTokensUseCase(
stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull()
}
multiYieldBalanceFetcher(
params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
multiStakingBalanceFetcher(
params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds),
)
}

View file

@ -0,0 +1,27 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:ShortArticle.kt$ShortArticle$val viewed: Boolean</ID>
<ID>BooleanPropertyNaming:TokenReceiveConfig.kt$TokenReceiveConfig$val showMemoDisclaimer: Boolean</ID>
<ID>BooleanPropertyNaming:UserWallet.kt$UserWallet.Hot$val backedUp: Boolean</ID>
<ID>BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs$val signatureVerification: Boolean?</ID>
<ID>BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs$val validatorAddress: Boolean?</ID>
<ID>BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs$val validatorAddresses: Boolean?</ID>
<ID>BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs.Amount$val required: Boolean</ID>
<ID>BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs.Duration$val required: Boolean</ID>
<ID>BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs.TronResource$val required: Boolean</ID>
<ID>CastNullableToNonNullableType:DerivationPathAdapterWithMigration.kt$DerivationPathAdapterWithMigration$as</ID>
<ID>MultilineLambdaItParameter:MobileWallet.kt$MobileWallet${ ExtendedPublicKey( publicKey = publicKey, chainCode = it, ) }</ID>
<ID>NoNameShadowing:Account.kt$Account.CryptoPortfolio.Companion$derivationIndex</ID>
<ID>NullableBooleanCheck:CryptoCurrency.kt$CryptoCurrency$iconUrl?.isNotBlank() ?: true</ID>
<ID>NullableToStringCall:AccountName.kt$AccountName.Error.Empty$${Empty::class.simpleName}</ID>
<ID>NullableToStringCall:AccountName.kt$AccountName.Error.ExceedsMaxLength$${ExceedsMaxLength::class.simpleName}</ID>
<ID>NullableToStringCall:DerivationIndex.kt$DerivationIndex.Error.NegativeDerivationIndex$${this::class.simpleName}</ID>
<ID>UnsafeCallOnNullableType:MobileWalletAsStringSerializer.kt$MobileWalletAsStringSerializer$moshi.adapter(MobileWallet::class.java).fromJson(decoder.decodeString())!!</ID>
<ID>UnsafeCallOnNullableType:ScanResponseAsStringSerializer.kt$ScanResponseAsStringSerializer$moshi.adapter(ScanResponse::class.java).fromJson(decoder.decodeString())!!</ID>
<ID>UseEmptyCounterpart:ScanResponse.kt$ScanResponse$mapOf()</ID>
<ID>UseOrEmpty:CardDTO.kt$CardDTO.FirmwareVersion$type.rawValue ?: ""</ID>
<ID>UseOrEmpty:UserWalletId.kt$UserWalletId$value?.toHexString() ?: ""</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -5,7 +5,7 @@ import com.tangem.domain.models.getResultStatusSource
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import kotlinx.serialization.Serializable
@ -56,13 +56,10 @@ data class CryptoCurrencyStatus(
/** The network address */
val networkAddress: NetworkAddress? get() = null
/** Staking yield balance */
val yieldBalance: YieldBalance? get() = null
/** Staking balance */
val stakingBalance: StakingBalance? get() = null
/**
* !!! DO NOT CONFUSE with STAKING YIELD BALANCE
* Yield supply status
*/
/** Yield supply status */
val yieldSupplyStatus: YieldSupplyStatus? get() = null
/** Sources */
@ -73,11 +70,11 @@ data class CryptoCurrencyStatus(
data class Sources(
val networkSource: StatusSource = StatusSource.ACTUAL,
val quoteSource: StatusSource = StatusSource.ACTUAL,
val yieldBalanceSource: StatusSource = StatusSource.ACTUAL,
val stakingBalanceSource: StatusSource = StatusSource.ACTUAL,
) {
val total: StatusSource by lazy {
listOf(networkSource, quoteSource, yieldBalanceSource).getResultStatusSource()
listOf(networkSource, quoteSource, stakingBalanceSource).getResultStatusSource()
}
}
@ -163,7 +160,7 @@ data class CryptoCurrencyStatus(
override val fiatAmount: SerializedBigDecimal,
override val fiatRate: SerializedBigDecimal,
override val priceChange: SerializedBigDecimal,
override val yieldBalance: YieldBalance?,
override val stakingBalance: StakingBalance?,
override val yieldSupplyStatus: YieldSupplyStatus?,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxInfo>,
@ -191,7 +188,7 @@ data class CryptoCurrencyStatus(
override val fiatAmount: SerializedBigDecimal?,
override val fiatRate: SerializedBigDecimal?,
override val priceChange: SerializedBigDecimal?,
override val yieldBalance: YieldBalance?,
override val stakingBalance: StakingBalance?,
override val yieldSupplyStatus: YieldSupplyStatus?,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxInfo>,
@ -213,7 +210,7 @@ data class CryptoCurrencyStatus(
@Serializable
data class NoQuote(
override val amount: SerializedBigDecimal,
override val yieldBalance: YieldBalance?,
override val stakingBalance: StakingBalance?,
override val yieldSupplyStatus: YieldSupplyStatus?,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxInfo>,

View file

@ -0,0 +1,19 @@
package com.tangem.domain.models.news
import kotlinx.serialization.Serializable
@Serializable
sealed class NewsError {
abstract val message: String?
abstract val code: Int?
data class ArticleNotFound(
override val message: String?,
override val code: Int?,
) : NewsError()
data class Unknown(
override val message: String?,
override val code: Int?,
) : NewsError()
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.models.news
import kotlinx.serialization.Serializable
/**
* Represents the result of fetching news.
* Can contain either data or an error.
*/
@Serializable
sealed interface TrendingNews {
@Serializable
data class Data(val articles: List<ShortArticle>) : TrendingNews
@Serializable
data class Error(val throwable: NewsError) : TrendingNews
}

View file

@ -0,0 +1,37 @@
package com.tangem.domain.models.staking
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/** P2P.org staking account */
@Serializable
data class P2PStakingAccount(
val delegatorAddress: String,
val vaultAddress: String,
val stake: P2PStake,
val availableToUnstake: SerializedBigDecimal,
val availableToWithdraw: SerializedBigDecimal,
val exitQueue: P2PExitQueue,
)
@Serializable
data class P2PStake(
val assets: SerializedBigDecimal,
val totalEarnedAssets: SerializedBigDecimal,
)
@Serializable
data class P2PExitQueue(
val total: SerializedBigDecimal,
val requests: List<P2PExitRequest>,
)
@Serializable
data class P2PExitRequest(
val ticket: String,
val totalAssets: SerializedBigDecimal,
val timestamp: Instant,
val withdrawalTimestamp: Instant,
val isClaimable: Boolean,
)

View file

@ -0,0 +1,101 @@
package com.tangem.domain.models.staking
import com.tangem.domain.models.StatusSource
import kotlinx.serialization.Serializable
import java.math.BigDecimal
/**
* Staking balance facade covering StakeKit and P2P balances
*/
@Serializable
sealed interface StakingBalance {
val stakingId: StakingID
val source: StatusSource
val totalStaked: BigDecimal
val totalRewards: BigDecimal?
val unstakingAmount: BigDecimal?
val withdrawableAmount: BigDecimal?
@Serializable
sealed interface Data : StakingBalance {
@Serializable
data class StakeKit(
override val stakingId: StakingID,
override val source: StatusSource,
val balance: YieldBalanceItem,
) : Data {
override val totalStaked: BigDecimal
get() = balance.items
.filter { it.type == BalanceType.STAKED }
.sumOf { it.amount }
override val totalRewards: BigDecimal
get() = balance.items
.filter { it.type == BalanceType.REWARDS }
.sumOf { it.amount }
override val unstakingAmount: BigDecimal
get() = balance.items
.filter { it.type == BalanceType.UNSTAKING || it.type == BalanceType.UNLOCKING }
.sumOf { it.amount }
override val withdrawableAmount: BigDecimal
get() = balance.items
.filter { it.type == BalanceType.UNSTAKED }
.sumOf { it.amount }
}
@Serializable
data class P2P(
override val stakingId: StakingID,
override val source: StatusSource,
val account: P2PStakingAccount,
) : Data {
override val totalStaked: BigDecimal
get() = account.stake.assets
override val totalRewards: BigDecimal
get() = account.stake.totalEarnedAssets
override val unstakingAmount: BigDecimal
get() = account.exitQueue.total
override val withdrawableAmount: BigDecimal
get() = account.availableToWithdraw
}
}
@Serializable
data class Empty(
override val stakingId: StakingID,
override val source: StatusSource,
) : StakingBalance {
override val totalStaked: BigDecimal get() = BigDecimal.ZERO
override val totalRewards: BigDecimal? get() = null
override val unstakingAmount: BigDecimal? get() = null
override val withdrawableAmount: BigDecimal? get() = null
}
@Serializable
data class Error(override val stakingId: StakingID) : StakingBalance {
override val source: StatusSource get() = StatusSource.ACTUAL
override val totalStaked: BigDecimal get() = BigDecimal.ZERO
override val totalRewards: BigDecimal? get() = null
override val unstakingAmount: BigDecimal? get() = null
override val withdrawableAmount: BigDecimal? get() = null
}
fun copySealed(source: StatusSource): StakingBalance {
return when (this) {
is Data.StakeKit -> copy(source = source)
is Data.P2P -> copy(source = source)
is Empty -> copy(source = source)
is Error -> this
}
}
}

View file

@ -3,4 +3,7 @@ package com.tangem.domain.models.staking
import kotlinx.serialization.Serializable
@Serializable
data class StakingID(val integrationId: String, val address: String)
data class StakingID(
val integrationId: String,
val address: String,
)

View file

@ -0,0 +1,8 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>UnnecessaryAbstractClass:MultiNetworkStatusSupplier.kt$MultiNetworkStatusSupplier$MultiNetworkStatusSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleNetworkStatusSupplier.kt$SingleNetworkStatusSupplier$SingleNetworkStatusSupplier</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -1,10 +1,10 @@
package com.tangem.domain.news.repository
import com.tangem.domain.news.model.NewsListBatchFlow
import com.tangem.domain.news.model.NewsListBatchingContext
import com.tangem.domain.models.news.ArticleCategory
import com.tangem.domain.models.news.DetailedArticle
import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.models.news.TrendingNews
import com.tangem.domain.news.model.NewsListBatchFlow
import com.tangem.domain.news.model.NewsListBatchingContext
import kotlinx.coroutines.flow.Flow
/**
@ -38,22 +38,17 @@ interface NewsRepository {
suspend fun fetchDetailedArticles(newsIds: Collection<Int>, language: String?)
/**
* Returns list of trending news by limit and with correct locale.
* Fetch list of trending news by limit and with correct locale and store it in runtime data store.
*
* @param limit
* @param language current device locale
*/
suspend fun getTrendingNews(limit: Int, language: String?): List<ShortArticle>
suspend fun fetchTrendingNews(limit: Int, language: String?)
/**
* Observes trending news with runtime viewed flag support.
*/
fun observeTrendingNews(): Flow<List<ShortArticle>>
/**
* Refreshes trending news list and updates cache without overriding viewed status.
*/
suspend fun refreshTrendingNews(limit: Int, language: String?)
fun observeTrendingNews(): Flow<TrendingNews>
/**
* Updates viewed flag for provided trending articles.

View file

@ -0,0 +1,23 @@
package com.tangem.domain.news.usecase
import arrow.core.Either
import com.tangem.domain.news.repository.NewsRepository
import java.util.Locale
/**
* Fetches trending news to store it in runtime data store.
*/
class FetchTrendingNewsUseCase(private val newsRepository: NewsRepository) {
suspend operator fun invoke(): Either<Throwable, Unit> = Either.catch {
newsRepository.fetchTrendingNews(
limit = LIMIT_FOR_TRENDING_NEWS,
language = Locale.getDefault().language,
)
}
companion object {
private const val LIMIT_FOR_TRENDING_NEWS = 10
}
}

View file

@ -1,8 +1,9 @@
package com.tangem.domain.news.usecase
import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.models.news.TrendingNews
import com.tangem.domain.news.repository.NewsRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
/**
* Exposes trending news as a cached flow and provides helpers to refresh or mark items as viewed.
@ -12,17 +13,12 @@ import kotlinx.coroutines.flow.Flow
class ManageTrendingNewsUseCase(private val repository: NewsRepository) {
/**
* Observes the current cached list of trending articles (max 10 items).
* Observes the current cached list of trending articles (max 10 items) or error.
*/
operator fun invoke(): Flow<List<ShortArticle>> {
return repository.observeTrendingNews()
}
/**
* Forces refresh from backend while preserving local `viewed` flags.
*/
suspend fun refresh(limit: Int, language: String?) {
repository.refreshTrendingNews(limit, language)
fun observeTrendingNews(): Flow<TrendingNews> {
return repository
.observeTrendingNews()
.distinctUntilChanged()
}
/**

View file

@ -17,7 +17,7 @@ dependencies {
// region Project Domain
implementation(projects.domain.core)
implementation(projects.domain.account.status)
implementation(projects.domain.account)
implementation(projects.domain.models)
implementation(projects.domain.networks)
implementation(projects.domain.nft.models)

View file

@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MultilineLambdaItParameter:NFTCollections.kt${ it.content is NFTCollections.Content.Error || it.content is NFTCollections.Content.Collections &amp;&amp; it.content.source == StatusSource.ONLY_CACHE }</ID>
<ID>MultilineLambdaItParameter:NFTCollections.kt${ val content = it.content content is NFTCollections.Content.Collections &amp;&amp; content.collections.isNullOrEmpty() }</ID>
<ID>MultilineLambdaItParameter:NFTCollections.kt${ val content = it.content content is NFTCollections.Content.Collections &amp;&amp; content.source != StatusSource.CACHE }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -1,15 +1,16 @@
package com.tangem.domain.nft
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.nft.utils.NFTCleaner
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.wallets.repository.WalletsRepository
class DisableWalletNFTUseCase(
private val walletsRepository: WalletsRepository,
private val nftRepository: NFTRepository,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val nftCleaner: NFTCleaner,
) {
suspend operator fun invoke(userWalletId: UserWalletId) {
@ -20,7 +21,7 @@ class DisableWalletNFTUseCase(
)
.orEmpty()
val networks = currencies.map { it.network }
nftRepository.clearCache(userWalletId, networks)
val networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network)
nftCleaner(userWalletId, networks)
}
}

View file

@ -1,9 +1,8 @@
package com.tangem.domain.nft
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTCollections
@ -16,7 +15,7 @@ import kotlinx.coroutines.flow.*
class GetNFTCollectionsUseCase(
private val currenciesRepository: CurrenciesRepository,
private val nftRepository: NFTRepository,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val accountsFeatureToggles: AccountsFeatureToggles,
) {
@ -33,13 +32,17 @@ class GetNFTCollectionsUseCase(
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun invokeForAccounts(userWalletId: UserWalletId): Flow<WalletNFTCollections> {
fun AccountStatus.flowOfNFTCollections(): Flow<Pair<Account, List<NFTCollections>>> =
nftCollections(userWalletId, this.flattenCurrencies().map { it.currency })
.map { nfts -> this.account to nfts }
fun Account.flowOfNFTCollections(): Flow<Pair<Account, List<NFTCollections>>> {
val currencies = (this as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty()
return singleAccountStatusListSupplier(userWalletId)
.mapLatest { statusList -> statusList.accountStatuses.map { it.flowOfNFTCollections() } }
return nftCollections(userWalletId = userWalletId, cryptoCurrencies = currencies.toList())
.map { nfts -> this to nfts }
}
return singleAccountListSupplier(userWalletId)
.mapLatest { statusList -> statusList.accounts.map { it.flowOfNFTCollections() } }
.flatMapLatest { flows -> combine(flows) { WalletNFTCollections(it.toMap()) } }
}

View file

@ -1,12 +1,14 @@
package com.tangem.domain.nft
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTNetworks
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
@ -14,19 +16,25 @@ import kotlinx.coroutines.flow.mapNotNull
class GetNFTNetworksUseCase(
private val currenciesRepository: CurrenciesRepository,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val nftRepository: NFTRepository,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(portfolioId: PortfolioId): Flow<NFTNetworks> = when (portfolioId) {
is PortfolioId.Account -> singleAccountStatusListSupplier(portfolioId.userWalletId)
.map { it.accountStatuses }
.mapNotNull { accountStatuses -> accountStatuses.find { it.account.accountId == portfolioId.accountId } }
.map { accountStatus -> accountStatus.flattenCurrencies().map { it.currency } }
.mapLatest { it.toNFTNetworks(portfolioId.userWalletId) }
is PortfolioId.Wallet ->
is PortfolioId.Account -> {
singleAccountListSupplier(portfolioId.userWalletId)
.mapNotNull { accountList ->
val account = accountList.accounts.find { it.accountId == portfolioId.accountId }
(account as? Account.CryptoPortfolio)?.cryptoCurrencies?.toList()
}
.mapLatest { it.toNFTNetworks(portfolioId.userWalletId) }
}
is PortfolioId.Wallet -> {
currenciesRepository
.getWalletCurrenciesUpdates(portfolioId.userWalletId)
.map { cryptoCurrencies -> cryptoCurrencies.toNFTNetworks(portfolioId.userWalletId) }
}
}
private suspend fun List<CryptoCurrency>.toNFTNetworks(userWalletId: UserWalletId): NFTNetworks {

View file

@ -6,12 +6,12 @@ import com.tangem.domain.account.supplier.SingleAccountListSupplier
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.nft.repository.NFTRepository
import com.tangem.domain.nft.utils.NFTCleaner
import com.tangem.domain.tokens.repository.CurrenciesRepository
import kotlinx.coroutines.flow.*
class ObserveAndClearNFTCacheIfNeedUseCase(
private val nftRepository: NFTRepository,
private val nftCleaner: NFTCleaner,
private val currenciesRepository: CurrenciesRepository,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val singleAccountListSupplier: SingleAccountListSupplier,
@ -26,7 +26,7 @@ class ObserveAndClearNFTCacheIfNeedUseCase(
.distinctUntilChanged()
.onEach { removedNetworks ->
if (removedNetworks.isNotEmpty()) {
nftRepository.clearCache(userWalletId, removedNetworks.toList())
nftCleaner(userWalletId, removedNetworks)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.nft.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.COLLECTIONS
import com.tangem.core.analytics.models.AnalyticsParam.Key.NFT
@ -18,7 +19,7 @@ sealed class NFTAnalyticsEvent(
) {
data class NFTListScreenOpened(
val state: State,
val state: AnalyticsParam.EmptyFull,
val collectionsCount: Int,
val allAssetsCount: Int,
val noCollectionAssetsCount: Int,
@ -30,15 +31,10 @@ sealed class NFTAnalyticsEvent(
put(NFT, allAssetsCount.toString())
put(NO_COLLECTION, noCollectionAssetsCount.toString())
},
) {
enum class State(val value: String) {
Empty("Empty"),
Full("Full"),
}
}
)
object Receive {
data object ScreenOpened : NFTAnalyticsEvent(event = "Receive NFT Screen Opened")
class ScreenOpened : NFTAnalyticsEvent(event = "Receive NFT Screen Opened")
data class BlockchainChosen(
private val blockchain: String,
@ -67,9 +63,9 @@ sealed class NFTAnalyticsEvent(
},
)
data object ButtonReadMore : NFTAnalyticsEvent(event = "Button - Read More")
data object ButtonSeeAll : NFTAnalyticsEvent(event = "Button - See All")
data object ButtonExplore : NFTAnalyticsEvent(event = "Button - Explore")
data object ButtonSend : NFTAnalyticsEvent(event = "Button - Send")
class ButtonReadMore : NFTAnalyticsEvent(event = "Button - Read More")
class ButtonSeeAll : NFTAnalyticsEvent(event = "Button - See All")
class ButtonExplore : NFTAnalyticsEvent(event = "Button - Explore")
class ButtonSend : NFTAnalyticsEvent(event = "Button - Send")
}
}

View file

@ -2,11 +2,11 @@ package com.tangem.domain.nft.repository
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.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
interface NFTRepository {
@ -32,6 +32,4 @@ interface NFTRepository {
suspend fun getNFTSupportedNetworks(userWalletId: UserWalletId): List<Network>
suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String?
suspend fun clearCache(userWalletId: UserWalletId, networks: List<Network>)
}

View file

@ -0,0 +1,30 @@
package com.tangem.domain.nft.utils
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
/**
* Cleans up NFT data for a given user wallet and network(s).
*
[REDACTED_AUTHOR]
*/
interface NFTCleaner {
/**
* Cleans up NFT data for a given user wallet and single network.
*
* @param userWalletId the user wallet id
* @param network the network to clean up
*/
suspend operator fun invoke(userWalletId: UserWalletId, network: Network) {
invoke(userWalletId = userWalletId, networks = setOf(network))
}
/**
* Cleans up NFT data for a given user wallet and multiple networks.
*
* @param userWalletId the user wallet id
* @param networks the set of networks to clean up
*/
suspend operator fun invoke(userWalletId: UserWalletId, networks: Set<Network>)
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>SuspendFunSwallowedCancellation:WasTwinsOnboardingShownUseCase.kt$WasTwinsOnboardingShownUseCase$runCatching</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MaxChainedCallsOnSameLine:GetOnrampOffersUseCase.kt$GetOnrampOffersUseCase$offer.quote.paymentMethod.type.getProcessingSpeed().speed</ID>
<ID>MultilineLambdaItParameter:GetOnrampQuotesUseCase.kt$GetOnrampQuotesUseCase${ when (it) { is OnrampQuote.Data -&gt; it.toAmount.value is OnrampQuote.Error -&gt; null // negative difference to sort both when data and unavailable is present is OnrampQuote.AmountError -&gt; { when (val error = it.error) { is OnrampError.AmountError.TooSmallError -&gt; it.fromAmount.value - error.requiredAmount is OnrampError.AmountError.TooBigError -&gt; error.requiredAmount - it.fromAmount.value } } } }</ID>
<ID>NamedArguments:GetOnrampOffersUseCase.kt$GetOnrampOffersUseCase$determineAdvantages( recentOffer, bestRateOffer, fastestOffer, isSingleOffer, )</ID>
<ID>UnnecessaryLet:OnrampAnalyticsEvent.kt$OnrampAnalyticsEvent.Errors$let { put(PAYMENT_METHOD, paymentMethod) }</ID>
<ID>UnnecessaryLet:OnrampAnalyticsEvent.kt$OnrampAnalyticsEvent.Errors$let { put(PROVIDER, providerName) }</ID>
<ID>UseEmptyCounterpart:OnrampAnalyticsEvent.kt$OnrampAnalyticsEvent$mapOf()</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:OnrampCountry.kt$OnrampCountry$val onrampAvailable: Boolean</ID>
<ID>ObjectExtendsThrowable:OnrampPairsError.kt$OnrampPairsError$PairsNotFound : OnrampPairsError</ID>
<ID>ObjectExtendsThrowable:OnrampRedirectError.kt$OnrampRedirectError$VerificationFailed : OnrampRedirectError</ID>
<ID>ObjectExtendsThrowable:OnrampRedirectError.kt$OnrampRedirectError$WrongRequestId : OnrampRedirectError</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -31,4 +31,6 @@ sealed class OnrampError {
) : OnrampError()
data object PairsNotFound : OnrampError()
data object AlreadyHandledTransaction : OnrampError()
}

View file

@ -96,9 +96,9 @@ class GetOnrampOffersUseCase(
isMoonpayPromoActive: Boolean,
): OnrampOffer? {
val moonpayPromoOffers = if (isMoonpayPromoActive) {
offers.filter {
it.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID &&
it.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY
offers.filter { offer ->
offer.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID &&
offer.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY
}
} else {
emptyList()

View file

@ -1,6 +1,7 @@
package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.left
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
@ -12,6 +13,10 @@ class GetOnrampTransactionUseCase(
) {
suspend operator fun invoke(txId: String): Either<OnrampError, OnrampTransaction> {
if (onrampTransactionRepository.isHandledTransaction(txId)) {
return OnrampError.AlreadyHandledTransaction.left()
}
return Either.catch {
requireNotNull(
onrampTransactionRepository.getTransactionById(txId),

View file

@ -11,10 +11,13 @@ class OnrampRemoveTransactionUseCase(
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(txId: String?): Either<OnrampError, Unit> {
suspend operator fun invoke(txId: String?, forceRemove: Boolean = false): Either<OnrampError, Unit> {
if (txId == null) return OnrampError.DomainError("Transaction id not provided").left()
return Either.catch {
if (!forceRemove) {
onrampTransactionRepository.storeHandledTransaction(txId)
}
onrampTransactionRepository.removeTransaction(txId)
}.mapLeft(errorResolver::resolve)
}

View file

@ -30,7 +30,7 @@ sealed class OnrampAnalyticsEvent(
),
)
data object SelectCurrencyScreenOpened : OnrampAnalyticsEvent(event = "Currency Screen Opened")
class SelectCurrencyScreenOpened : OnrampAnalyticsEvent(event = "Currency Screen Opened")
data class FiatCurrencyChosen(
private val currency: String,
@ -39,11 +39,11 @@ sealed class OnrampAnalyticsEvent(
params = mapOf("Currency Type" to currency),
)
data object CloseOnramp : OnrampAnalyticsEvent(event = "Button - Close")
class CloseOnramp : OnrampAnalyticsEvent(event = "Button - Close")
data object SettingsOpened : OnrampAnalyticsEvent(event = "Onramp Settings Screen Opened")
class SettingsOpened : OnrampAnalyticsEvent(event = "Onramp Settings Screen Opened")
data object SelectResidenceOpened : OnrampAnalyticsEvent(event = "Residence Screen Opened")
class SelectResidenceOpened : OnrampAnalyticsEvent(event = "Residence Screen Opened")
data class OnResidenceChosen(
private val residence: String,
@ -59,7 +59,7 @@ sealed class OnrampAnalyticsEvent(
params = mapOf(RESIDENCE to residence),
)
data object OnResidenceChange : OnrampAnalyticsEvent(event = "Button - Change")
class OnResidenceChange : OnrampAnalyticsEvent(event = "Button - Change")
data class OnResidenceConfirm(
private val residence: String,
@ -68,7 +68,7 @@ sealed class OnrampAnalyticsEvent(
params = mapOf(RESIDENCE to residence),
)
data object ProvidersScreenOpened : OnrampAnalyticsEvent(event = "Providers Screen Opened")
class ProvidersScreenOpened : OnrampAnalyticsEvent(event = "Providers Screen Opened")
data class ProviderCalculated(
private val providerName: String,
@ -83,7 +83,7 @@ sealed class OnrampAnalyticsEvent(
),
)
data object PaymentMethodsScreenOpened : OnrampAnalyticsEvent(event = "Payment Method Screen Opened")
class PaymentMethodsScreenOpened : OnrampAnalyticsEvent(event = "Payment Method Screen Opened")
data class OnPaymentMethodChosen(
private val paymentMethod: String,
@ -135,8 +135,8 @@ sealed class OnrampAnalyticsEvent(
),
)
data object MinAmountError : OnrampAnalyticsEvent(event = "Error - Min Amount")
data object MaxAmountError : OnrampAnalyticsEvent(event = "Error - Max Amount")
class MinAmountError : OnrampAnalyticsEvent(event = "Error - Min Amount")
class MaxAmountError : OnrampAnalyticsEvent(event = "Error - Max Amount")
data class Errors(
private val tokenSymbol: String,
@ -203,7 +203,7 @@ sealed class OnrampAnalyticsEvent(
),
)
data object AllOffersClicked : OnrampAnalyticsEvent(
class AllOffersClicked : OnrampAnalyticsEvent(
event = "Button - All Offers",
params = emptyMap(),
)

View file

@ -24,4 +24,8 @@ interface OnrampTransactionRepository {
)
suspend fun removeTransaction(txId: String)
suspend fun storeHandledTransaction(txId: String)
suspend fun isHandledTransaction(txId: String): Boolean
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>NullableBooleanCheck:GetStoryContentUseCase.kt$GetStoryContentUseCase$isFCAAllowed(id).firstOrNull() ?: false</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -16,7 +16,7 @@ interface PromoRepository {
suspend fun setNeverToShowTokenPromo(promoId: PromoId)
suspend fun isMarketsStakingNotificationHideClicked(): Flow<Boolean>
fun isMarketsStakingNotificationHideClicked(): Flow<Boolean>
suspend fun setMarketsStakingNotificationHideClicked()

View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>UnnecessaryAbstractClass:SingleQuoteStatusSupplier.kt$SingleQuoteStatusSupplier$SingleQuoteStatusSupplier</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -2,13 +2,12 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CanBeNonNullable:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$value: Any?</ID>
<ID>MultilineLambdaItParameter:FetchStakingYieldBalanceUseCase.kt$FetchStakingYieldBalanceUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -&gt; raise(StakingError.DomainError("$it")) StakingIdFactory.Error.UnsupportedCurrency -&gt; Unit.right() } return@either }</ID>
<ID>MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending &amp;&amp; action.amount &lt; it.amount &amp;&amp; it.type == BalanceType.STAKED &amp;&amp; it.validatorAddress == action.validatorAddress }</ID>
<ID>NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId)</ID>
<ID>NullableToStringCall:StakingApyFlowUseCase.kt$StakingApyFlowUseCase$${yield.token.coinGeckoId}</ID>
<ID>UnnecessaryAbstractClass:MultiYieldBalanceSupplier.kt$MultiYieldBalanceSupplier$MultiYieldBalanceSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleYieldBalanceSupplier.kt$SingleYieldBalanceSupplier$SingleYieldBalanceSupplier</ID>
<ID>UnnecessaryAbstractClass:MultiStakingBalanceSupplier.kt$MultiStakingBalanceSupplier$MultiStakingBalanceSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleStakingBalanceSupplier.kt$SingleStakingBalanceSupplier$SingleStakingBalanceSupplier</ID>
<ID>UseEmptyCounterpart:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$mapOf()</ID>
<ID>UseOrEmpty:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase$action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: ""</ID>
</CurrentIssues>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:P2PEthPoolStaking.kt$P2PEthPoolStaking.Metadata.Fee$val enabled: Boolean</ID>
<ID>BooleanPropertyNaming:P2PEthPoolStaking.kt$P2PEthPoolStaking.Status$val enter: Boolean</ID>
<ID>BooleanPropertyNaming:P2PEthPoolStaking.kt$P2PEthPoolStaking.Status$val exit: Boolean</ID>
<ID>BooleanPropertyNaming:StakingActionCommonType.kt$StakingActionCommonType.Enter$val skipEnterAmount: Boolean</ID>
<ID>BooleanPropertyNaming:StakingActionCommonType.kt$StakingActionCommonType.Exit$val partiallyUnstakeDisabled: Boolean</ID>
<ID>BooleanPropertyNaming:StakingActionCommonType.kt$StakingActionCommonType.Pending.Stake$val skipEnterAmount: Boolean</ID>
<ID>BooleanPropertyNaming:Yield.kt$AddressArgument$val required: Boolean</ID>
<ID>BooleanPropertyNaming:Yield.kt$Yield$val allValidatorsFull: Boolean = validators.all { it.status == Validator.ValidatorStatus.FULL }</ID>
<ID>BooleanPropertyNaming:Yield.kt$Yield.Metadata$val supportsMultipleValidators: Boolean?</ID>
<ID>BooleanPropertyNaming:Yield.kt$Yield.Metadata.Enabled$val enabled: Boolean</ID>
<ID>BooleanPropertyNaming:Yield.kt$Yield.Status$val enter: Boolean</ID>
<ID>BooleanPropertyNaming:Yield.kt$Yield.Status$val exit: Boolean?</ID>
<ID>BooleanPropertyNaming:Yield.kt$Yield.Validator$val preferred: Boolean</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -39,12 +39,11 @@ sealed interface StakingOption {
* P2P pooled staking option
* Wraps P2P ETH Pool vault information
*/
data class P2P(val vault: P2PEthPoolVault) : StakingOption {
override val integrationId: String =
"p2p-ethereum-pooled:${vault.vaultAddress}"
override val apy: SerializedBigDecimal = vault.apy
data class P2P(val vaults: List<P2PEthPoolVault>) : StakingOption {
override val integrationId: String = "p2p-ethereum-pooled"
override val apy: SerializedBigDecimal = vaults.maxOf { it.apy }
override val token: YieldToken = createEthToken()
override val isAvailable: Boolean = !vault.isPrivate
override val isAvailable: Boolean = vaults.isNotEmpty()
private fun createEthToken(): YieldToken { // TODO
return YieldToken(

View file

@ -11,6 +11,8 @@ enum class P2PEthPoolNetwork(
val value: String,
val displayName: String,
val chainId: Int,
val stakingNetworkId: String,
val isTestnet: Boolean,
) {
/**
* Ethereum mainnet
@ -20,16 +22,20 @@ enum class P2PEthPoolNetwork(
value = "mainnet",
displayName = "Ethereum",
chainId = 1,
stakingNetworkId = "ethereum",
isTestnet = false,
),
/**
* Ethereum testnet (Holesky)
* Ethereum testnet (Hoodi)
* Chain ID: 17000
*/
TESTNET(
value = "hoodi",
displayName = "Holesky Testnet",
displayName = "Hoodi Testnet",
chainId = 17000,
stakingNetworkId = "ethereum/test",
isTestnet = true,
),
;

View file

@ -0,0 +1,14 @@
package com.tangem.domain.staking.model.ethpool
/**
* Configuration for P2P Ethereum staking network.
*
* Change [USE_TESTNET] to switch between testnet and mainnet.
*/
object P2PStakingConfig {
const val USE_TESTNET: Boolean = true
val activeNetwork: P2PEthPoolNetwork
get() = if (USE_TESTNET) P2PEthPoolNetwork.TESTNET else P2PEthPoolNetwork.MAINNET
}

View file

@ -7,10 +7,10 @@ import arrow.core.right
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
class FetchStakingYieldBalanceUseCase(
private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
private val stakingIdFactory: StakingIdFactory,
) {
@ -32,8 +32,8 @@ class FetchStakingYieldBalanceUseCase(
return@either
}
singleYieldBalanceFetcher(
params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId),
singleStakingBalanceFetcher(
params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId),
)
.mapLeft { StakingError.DomainError("$it") }
.bind()

View file

@ -2,7 +2,6 @@ package com.tangem.domain.staking.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent.ButtonRewards.addIfValueIsNotNull
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.models.staking.action.StakingActionType
@ -24,11 +23,11 @@ sealed class StakingAnalyticsEvent(
),
)
data object WhatIsStaking : StakingAnalyticsEvent(
class WhatIsStaking : StakingAnalyticsEvent(
event = "Link - What Is Staking",
)
data object AmountScreenOpened : StakingAnalyticsEvent(
class AmountScreenOpened : StakingAnalyticsEvent(
event = "Amount Screen Opened",
)
@ -54,7 +53,7 @@ sealed class StakingAnalyticsEvent(
),
)
data object RewardScreenOpened : StakingAnalyticsEvent(
class RewardScreenOpened : StakingAnalyticsEvent(
event = "Reward Screen Opened",
)
@ -67,7 +66,7 @@ sealed class StakingAnalyticsEvent(
),
)
data object ButtonMax : StakingAnalyticsEvent(
class ButtonMax : StakingAnalyticsEvent(
event = "Button - Max",
)
@ -98,7 +97,7 @@ sealed class StakingAnalyticsEvent(
),
)
data object ButtonRewards : StakingAnalyticsEvent(
class ButtonRewards : StakingAnalyticsEvent(
event = "Button - Rewards",
)
@ -112,9 +111,9 @@ sealed class StakingAnalyticsEvent(
),
)
data object ButtonShare : StakingAnalyticsEvent(event = "Button - Share")
class ButtonShare : StakingAnalyticsEvent(event = "Button - Share")
data object ButtonExplore : StakingAnalyticsEvent(event = "Button - Explore")
class ButtonExplore : StakingAnalyticsEvent(event = "Button - Explore")
data class StakeKitApiError(
val stakingError: StakingError.StakeKitApiError,
@ -145,12 +144,6 @@ sealed class StakingAnalyticsEvent(
},
)
fun MutableMap<String, String>.addIfValueIsNotNull(key: String, value: Any?) {
if (value != null) {
put(key, value.toString())
}
}
data class TransactionError(
val errorCode: String,
) : StakingAnalyticsEvent(
@ -178,4 +171,11 @@ sealed class StakingAnalyticsEvent(
enum class StakeScreenSource {
Info, Amount, Confirmation, Validators,
}
@Suppress("CanBeNonNullable")
fun MutableMap<String, String>.addIfValueIsNotNull(key: String, value: Any?) {
if (value != null) {
put(key, value.toString())
}
}

View file

@ -5,17 +5,17 @@ import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
/**
* Params for fetchers of yield balance
* Params for fetchers of staking balance
*
[REDACTED_AUTHOR]
*/
sealed interface YieldBalanceFetcherParams {
sealed interface StakingBalanceFetcherParams {
/** User wallet ID */
val userWalletId: UserWalletId
/**
* Params for fetching multiple yield balances
* Params for fetching multiple staking balances
*
* @property userWalletId user wallet ID
* @property currencyIdWithNetworkMap map of currency ID to network
@ -23,10 +23,10 @@ sealed interface YieldBalanceFetcherParams {
data class Multi(
override val userWalletId: UserWalletId,
val currencyIdWithNetworkMap: Map<CryptoCurrency.ID, Network>,
) : YieldBalanceFetcherParams
) : StakingBalanceFetcherParams
/**
* Params for fetching single yield balance
* Params for fetching single staking balance
*
* @property userWalletId user wallet ID
* @property currencyId currency ID
@ -36,5 +36,5 @@ sealed interface YieldBalanceFetcherParams {
override val userWalletId: UserWalletId,
val currencyId: CryptoCurrency.ID,
val network: Network,
) : YieldBalanceFetcherParams
) : StakingBalanceFetcherParams
}

View file

@ -5,6 +5,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toMigratedCoinId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
/**
* Represents a staking integration identifier.
@ -101,8 +102,10 @@ sealed interface StakingIntegrationID {
enum class P2P : StakingIntegrationID {
EthereumPooled {
override val value: String = "p2p-ethereum-pooled"
override val blockchain: Blockchain = Blockchain.Ethereum
override val networkId: String = "ethereum"
override val blockchain: Blockchain
get() = if (P2PStakingConfig.USE_TESTNET) Blockchain.EthereumTestnet else Blockchain.Ethereum
override val networkId: String
get() = P2PStakingConfig.activeNetwork.stakingNetworkId
},
}

View file

@ -5,14 +5,14 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.staking.StakingID
/**
* Fetcher of yields balances
* Fetcher of staking balances
*
[REDACTED_AUTHOR]
*/
interface MultiYieldBalanceFetcher : FlowFetcher<MultiYieldBalanceFetcher.Params> {
interface MultiStakingBalanceFetcher : FlowFetcher<MultiStakingBalanceFetcher.Params> {
/**
* Params for fetching multiple yield balances
* Params for fetching multiple staking balances
*
* @property userWalletId user wallet ID
* @property stakingIds map of currency ID to network
@ -24,7 +24,7 @@ interface MultiYieldBalanceFetcher : FlowFetcher<MultiYieldBalanceFetcher.Params
override fun toString(): String {
return """
MultiYieldBalanceFetcher.Params(
MultiStakingBalanceFetcher.Params(
userWalletId = $userWalletId,
stakingIds: ${stakingIds.joinToString()}
)

View file

@ -0,0 +1,17 @@
package com.tangem.domain.staking.multi
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.wallet.UserWalletId
/**
* Producer of all staking balances for selected wallet [UserWalletId]
*
[REDACTED_AUTHOR]
*/
interface MultiStakingBalanceProducer : FlowProducer<Set<StakingBalance>> {
data class Params(val userWalletId: UserWalletId)
interface Factory : FlowProducer.Factory<Params, MultiStakingBalanceProducer>
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.staking.multi
import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.staking.StakingBalance
/**
* Supplier of all staking balances for selected wallet [MultiStakingBalanceProducer.Params]
*
* @property factory factory for creating [MultiStakingBalanceProducer]
* @property keyCreator key creator
*
[REDACTED_AUTHOR]
*/
abstract class MultiStakingBalanceSupplier(
override val factory: FlowProducer.Factory<MultiStakingBalanceProducer.Params, MultiStakingBalanceProducer>,
override val keyCreator: (MultiStakingBalanceProducer.Params) -> String,
) : FlowCachingSupplier<MultiStakingBalanceProducer, MultiStakingBalanceProducer.Params, Set<StakingBalance>>()

View file

@ -1,17 +0,0 @@
package com.tangem.domain.staking.multi
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.wallet.UserWalletId
/**
* Producer of all yield balances for selected wallet [UserWalletId]
*
[REDACTED_AUTHOR]
*/
interface MultiYieldBalanceProducer : FlowProducer<Set<YieldBalance>> {
data class Params(val userWalletId: UserWalletId)
interface Factory : FlowProducer.Factory<Params, MultiYieldBalanceProducer>
}

View file

@ -1,18 +0,0 @@
package com.tangem.domain.staking.multi
import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.staking.YieldBalance
/**
* Supplier of all yield balances for selected wallet [MultiYieldBalanceProducer.Params]
*
* @property factory factory for creating [MultiYieldBalanceProducer]
* @property keyCreator key creator
*
[REDACTED_AUTHOR]
*/
abstract class MultiYieldBalanceSupplier(
override val factory: FlowProducer.Factory<MultiYieldBalanceProducer.Params, MultiYieldBalanceProducer>,
override val keyCreator: (MultiYieldBalanceProducer.Params) -> String,
) : FlowCachingSupplier<MultiYieldBalanceProducer, MultiYieldBalanceProducer.Params, Set<YieldBalance>>()

View file

@ -2,7 +2,13 @@ package com.tangem.domain.staking.repositories
import arrow.core.Either
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.ethpool.*
import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount
import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastResult
import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork
import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.stakekit.StakingError
import kotlinx.coroutines.flow.Flow
@ -13,7 +19,7 @@ interface P2PEthPoolRepository {
*
* @param network P2P network (MAINNET or TESTNET)
*/
suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PEthPoolNetwork.MAINNET)
suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork)
/**
* Get list of available staking vaults
@ -22,7 +28,7 @@ interface P2PEthPoolRepository {
* @return Either error or list of vaults with APY, capacity, fees
*/
suspend fun getVaults(
network: P2PEthPoolNetwork = P2PEthPoolNetwork.MAINNET,
network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork,
): Either<StakingError, List<P2PEthPoolVault>>
/**

View file

@ -5,14 +5,14 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.staking.StakingID
/**
* Fetcher of yield balance
* Fetcher of staking balance
*
[REDACTED_AUTHOR]
*/
interface SingleYieldBalanceFetcher : FlowFetcher<SingleYieldBalanceFetcher.Params> {
interface SingleStakingBalanceFetcher : FlowFetcher<SingleStakingBalanceFetcher.Params> {
/**
* Params for fetching single yield balance
* Params for fetching single staking balance
*
* @property userWalletId user wallet ID
* @property stakingId staking ID

View file

@ -2,15 +2,15 @@ package com.tangem.domain.staking.single
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.staking.YieldBalance
/**
* Producer of yield balance for selected wallet [UserWalletId]
* Producer of staking balance for selected wallet [UserWalletId]
*
[REDACTED_AUTHOR]
*/
interface SingleYieldBalanceProducer : FlowProducer<YieldBalance> {
interface SingleStakingBalanceProducer : FlowProducer<StakingBalance> {
data class Params(
val userWalletId: UserWalletId,
@ -19,7 +19,7 @@ interface SingleYieldBalanceProducer : FlowProducer<YieldBalance> {
override fun toString(): String {
return """
SingleYieldBalanceProducer.Params(
SingleStakingBalanceProducer.Params(
userWalletId = $userWalletId,
stakingId = $stakingId,
)
@ -27,5 +27,5 @@ interface SingleYieldBalanceProducer : FlowProducer<YieldBalance> {
}
}
interface Factory : FlowProducer.Factory<Params, SingleYieldBalanceProducer>
interface Factory : FlowProducer.Factory<Params, SingleStakingBalanceProducer>
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.staking.single
import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.staking.StakingBalance
/**
* Supplier of staking balance for selected wallet [SingleStakingBalanceProducer.Params]
*
* @property factory factory for creating [SingleStakingBalanceProducer]
* @property keyCreator key creator
*
[REDACTED_AUTHOR]
*/
abstract class SingleStakingBalanceSupplier(
override val factory: FlowProducer.Factory<SingleStakingBalanceProducer.Params, SingleStakingBalanceProducer>,
override val keyCreator: (SingleStakingBalanceProducer.Params) -> String,
) : FlowCachingSupplier<SingleStakingBalanceProducer, SingleStakingBalanceProducer.Params, StakingBalance>()

View file

@ -1,18 +0,0 @@
package com.tangem.domain.staking.single
import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.staking.YieldBalance
/**
* Supplier of yield balance for selected wallet [SingleYieldBalanceProducer.Params]
*
* @property factory factory for creating [SingleYieldBalanceProducer]
* @property keyCreator key creator
*
[REDACTED_AUTHOR]
*/
abstract class SingleYieldBalanceSupplier(
override val factory: FlowProducer.Factory<SingleYieldBalanceProducer.Params, SingleYieldBalanceProducer>,
override val keyCreator: (SingleYieldBalanceProducer.Params) -> String,
) : FlowCachingSupplier<SingleYieldBalanceProducer, SingleYieldBalanceProducer.Params, YieldBalance>()

View file

@ -0,0 +1,89 @@
package com.tangem.domain.staking.utils
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.lib.crypto.BlockchainUtils
import java.math.BigDecimal
/**
* Provider-agnostic extension to get total balance including rewards.
* Works for both StakeKit and P2P providers.
*
* Returns sum of all staking-related balances including rewards
* (staked + unstaking + withdrawable + rewards).
*
* When [BlockchainUtils.isIncludeStakingTotalBalance] is false, the staked balance
* is already included in the main wallet balance, so we only return rewards.
*/
fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal {
return when (this) {
is StakingBalance.Data.StakeKit -> getTotalWithRewardsStakingBalanceStakeKit(blockchainId)
is StakingBalance.Data.P2P -> {
val rewards = totalRewards
if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId)) {
totalStaked + unstakingAmount + withdrawableAmount + rewards
} else {
rewards
}
}
}
}
/**
* Provider-agnostic extension to get total staking balance excluding rewards.
* Works for both StakeKit and P2P providers.
*
* Returns sum of all staking-related balances (staked + unstaking + withdrawable)
* excluding rewards.
*/
fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal {
return when (this) {
is StakingBalance.Data.StakeKit -> getTotalStakingBalanceStakeKit(blockchainId)
is StakingBalance.Data.P2P -> totalStaked + unstakingAmount + withdrawableAmount
}
}
/**
* StakeKit-specific extension to get total balance including rewards.
*/
private fun StakingBalance.Data.StakeKit.getTotalWithRewardsStakingBalanceStakeKit(blockchainId: String): BigDecimal {
return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) {
balance.items.sumOf { it.amount }
} else {
getRewardStakingBalance()
}
}
/**
* StakeKit-specific extension to get total staked balance excluding rewards.
*/
private fun StakingBalance.Data.StakeKit.getTotalStakingBalanceStakeKit(blockchainId: String): BigDecimal {
return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) {
balance.items
.filterNot { it.type == BalanceType.REWARDS }
.sumOf { it.amount }
} else {
balance.items
.filterNot { it.type == BalanceType.REWARDS }
.sumOf { it.amount } - getRewardStakingBalance()
}
}
/**
* StakeKit-specific extension to get reward balance.
*/
fun StakingBalance.Data.StakeKit.getRewardStakingBalance(): BigDecimal {
return balance.items
.filter { it.type == BalanceType.REWARDS }
.sumOf { it.amount }
}
/**
* StakeKit-specific extension to get validators count.
*/
fun StakingBalance.Data.StakeKit.getValidatorsCount(): Int {
return balance.items
.filterNot { it.validatorAddress.isNullOrBlank() }
.distinctBy { it.validatorAddress }
.size
}

View file

@ -1,39 +0,0 @@
package com.tangem.domain.staking.utils
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.lib.crypto.BlockchainUtils
import java.math.BigDecimal
fun YieldBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal {
return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) {
balance.items.sumOf { it.amount }
} else {
getRewardStakingBalance()
}
}
fun YieldBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal {
return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) {
balance.items
.filterNot { it.type == BalanceType.REWARDS }
.sumOf { it.amount }
} else {
balance.items
.filterNot { it.type == BalanceType.REWARDS }
.sumOf { it.amount } - getRewardStakingBalance()
}
}
fun YieldBalance.Data.getRewardStakingBalance(): BigDecimal {
return balance.items
.filter { it.type == BalanceType.REWARDS }
.sumOf { it.amount }
}
fun YieldBalance.Data.getValidatorsCount(): Int {
return balance.items
.filterNot { it.validatorAddress.isNullOrBlank() }
.distinctBy { it.validatorAddress }
.size
}

View file

@ -149,7 +149,7 @@ internal class StakingIdFactoryTest {
expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano),
),
CreateModel(
currencyId = createCurrencyId(blockchain = Blockchain.Ethereum),
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain),
expected = createStakingId(integrationId = StakingIntegrationID.P2P.EthereumPooled),
),
CreateModel(

Some files were not shown because too many files have changed in this diff Show more