Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-13 13:02:12 +03:00
commit 3946e4ebca
359 changed files with 8459 additions and 3324 deletions

View file

@ -24,6 +24,7 @@ dependencies {
// region Project - Domain
api(projects.domain.account)
api(projects.domain.card)
api(projects.domain.common)
api(projects.domain.models)
// endregion

View file

@ -28,7 +28,7 @@ internal class AccountListConverter @AssistedInject constructor(
override fun convert(value: GetWalletAccountsResponse): AccountList {
return AccountList(
userWallet = userWallet,
userWalletId = userWallet.walletId,
accounts = value.accounts.map(cryptoPortfolioConverter::convert).toSet(),
totalAccounts = value.wallet.totalAccounts,
sortType = TokensSortTypeConverter.convert(value.wallet.sort),

View file

@ -1,7 +1,6 @@
package com.tangem.data.account.converter
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.account.Account
import com.tangem.utils.converter.Converter
@ -21,8 +20,8 @@ internal object SaveWalletAccountsResponseConverter : Converter<AccountList, Sav
)
}
private fun toDTO(account: Account.CryptoPortfolio): WalletAccountDTO {
return WalletAccountDTO(
private fun toDTO(account: Account.CryptoPortfolio): SaveWalletAccountsResponse.AccountDTO {
return SaveWalletAccountsResponse.AccountDTO(
id = account.accountId.value,
name = AccountNameConverter.convert(value = account.accountName),
derivationIndex = account.derivationIndex.value,

View file

@ -2,6 +2,7 @@ package com.tangem.data.account.fetcher
import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.assignTokens
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.account.WalletAccountsFetcher
@ -14,6 +15,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.
import com.tangem.datasource.api.common.response.ETAG_HEADER
import com.tangem.datasource.api.common.response.isNetworkError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
@ -31,17 +33,20 @@ import javax.inject.Singleton
* @property accountsResponseStoreFactory factory to create [AccountsResponseStore]
* @property userTokensSaver saves user tokens to the database
* @property fetchWalletAccountsErrorHandler handles errors during fetching wallet accounts
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
* @property eTagsStore store for ETags to manage caching
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
@Singleton
internal class DefaultWalletAccountsFetcher @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
private val userTokensSaver: UserTokensSaver,
private val fetchWalletAccountsErrorHandler: FetchWalletAccountsErrorHandler,
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
private val eTagsStore: ETagsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletAccountsFetcher, WalletAccountsSaver {
@ -49,30 +54,33 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
override suspend fun fetch(userWalletId: UserWalletId) {
val savedAccountsResponse = getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
val accountsResponse = fetchWalletAccounts(userWalletId, savedAccountsResponse)
val unassignedTokens = accountsResponse?.unassignedTokens
?: return
if (!unassignedTokens.isNullOrEmpty()) {
if (accountsResponse.accounts.isEmpty()) {
initializeAccounts(userWalletId, accountsResponse)
} else if (accountsResponse.unassignedTokens.isNotEmpty()) {
assignTokens(userWalletId, accountsResponse)
}
}
override suspend fun pushAndStore(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
push(userWalletId = userWalletId, accounts = response.accounts)
store(userWalletId = userWalletId, response = response)
}
override suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
val store = getAccountsResponseStore(userWalletId = userWalletId)
store.updateData { response }
}
override suspend fun push(userWalletId: UserWalletId, accounts: List<WalletAccountDTO>) {
push(userWalletId = userWalletId, body = SaveWalletAccountsResponse(accounts = accounts))
override suspend fun push(
userWalletId: UserWalletId,
accounts: List<WalletAccountDTO>,
): GetWalletAccountsResponse? {
return push(userWalletId = userWalletId, body = SaveWalletAccountsResponse(accounts = accounts))
}
override suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse) {
safeApiCall(
override suspend fun push(
userWalletId: UserWalletId,
body: SaveWalletAccountsResponse,
): GetWalletAccountsResponse? {
return safeApiCall(
call = {
var eTag = getETag(userWalletId)
@ -86,11 +94,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
tangemTechApi.saveWalletAccounts(
walletId = userWalletId.stringValue,
eTag = eTag,
body = body.copy(
accounts = body.accounts.map {
it.copy(tokens = null, totalTokens = null, totalNetworks = null)
},
),
body = body,
)
}
@ -102,6 +106,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
if (error.isNetworkError(code = Code.PRECONDITION_FAILED)) {
throw error
}
null
},
)
}
@ -135,12 +141,28 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
pushWalletAccounts = ::push,
storeWalletAccounts = ::store,
)
null
},
)
}
private suspend fun initializeAccounts(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
val response = defaultWalletAccountsResponseFactory.create(
userWalletId = userWalletId,
userTokensResponse = UserTokensResponse(
group = accountsResponse.wallet.group,
sort = accountsResponse.wallet.sort,
tokens = accountsResponse.unassignedTokens,
),
)
userTokensSaver.push(userWalletId = userWalletId, response = response.toUserTokensResponse())
val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts)
if (syncedResponse != null) {
store(userWalletId = userWalletId, response = syncedResponse)
}
}
private suspend fun assignTokens(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
val accountsResponseWithTokens = accountsResponse.assignTokens(userWalletId)

View file

@ -1,10 +1,8 @@
package com.tangem.data.account.fetcher
import com.tangem.data.account.converter.CryptoPortfolioConverter
import com.tangem.data.account.utils.assignTokens
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
@ -13,10 +11,6 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import timber.log.Timber
import javax.inject.Inject
@ -24,12 +18,9 @@ import javax.inject.Inject
/**
* Handles errors that occur during the fetching of wallet accounts
*
* @property userTokensSaver saves user tokens to the storage
* @property userWalletsStore provides access to user wallet data
* @property userTokensResponseStore provides access to user token responses.
* @property cryptoPortfolioCF factory for converting crypto portfolios
* @property userTokensResponseFactory factory for creating user token responses
* @property cardCryptoCurrencyFactory factory for creating default cryptocurrencies for multi-currency wallets
* @property userTokensSaver saves user tokens to the storage
* @property userTokensResponseStore provides access to user token responses.
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
*
* @see DefaultWalletAccountsFetcher
*
@ -37,11 +28,8 @@ import javax.inject.Inject
*/
internal class FetchWalletAccountsErrorHandler @Inject constructor(
private val userTokensSaver: UserTokensSaver,
private val userWalletsStore: UserWalletsStore,
private val userTokensResponseStore: UserTokensResponseStore,
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory,
private val userTokensResponseFactory: UserTokensResponseFactory,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
) {
/**
@ -59,72 +47,47 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
error: ApiResponseError,
userWalletId: UserWalletId,
savedAccountsResponse: GetWalletAccountsResponse?,
pushWalletAccounts: suspend (userWalletId: UserWalletId, accounts: List<WalletAccountDTO>) -> Unit,
storeWalletAccounts: suspend (userWalletId: UserWalletId, response: GetWalletAccountsResponse) -> Unit,
) {
pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse?,
storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit,
): GetWalletAccountsResponse? {
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
if (isResponseUpToDate) {
Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
return
return savedAccountsResponse
}
val (accountDTOs, userTokensResponse) = if (savedAccountsResponse == null) {
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
createDefaultAccountDTOs(userWallet) to getFromLegacyStore(userWalletId).orDefault(userWallet)
} else {
savedAccountsResponse.accounts to savedAccountsResponse.toUserTokensResponse()
}
var response = savedAccountsResponse ?: createDefaultResponse(userWalletId)
val (accountDTOs, userTokensResponse) = response.accounts to response.toUserTokensResponse()
val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND)
if (isNotFoundError) {
pushWalletAccounts(userWalletId, accountDTOs)
userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse)
val updatedResponse = pushWalletAccounts(userWalletId, accountDTOs)
if (updatedResponse != null) {
response = updatedResponse
}
}
val response = savedAccountsResponse.orDefault(userWalletId, accountDTOs, userTokensResponse)
storeWalletAccounts(userWalletId, response)
return response
}
private fun createDefaultAccountDTOs(userWallet: UserWallet): List<WalletAccountDTO> {
val accounts = AccountList.empty(userWallet).accounts
.filterIsInstance<Account.CryptoPortfolio>()
val converter = cryptoPortfolioCF.create(userWallet = userWallet)
return converter.convertListBack(input = accounts)
private suspend fun createDefaultResponse(userWalletId: UserWalletId): GetWalletAccountsResponse {
return defaultWalletAccountsResponseFactory.create(
userWalletId = userWalletId,
userTokensResponse = getFromLegacyStore(userWalletId),
)
}
private suspend fun getFromLegacyStore(userWalletId: UserWalletId): UserTokensResponse? {
return userTokensResponseStore.getSyncOrNull(userWalletId)
?.let {
it.copy(
tokens = UserTokensResponseAccountIdEnricher(userWalletId = userWalletId, tokens = it.tokens),
)
}
.also { userTokensResponseStore.clear(userWalletId) }
}
private fun UserTokensResponse?.orDefault(userWallet: UserWallet): UserTokensResponse {
if (this != null) return this
return userTokensResponseFactory.createUserTokensResponse(
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet = userWallet),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
}
private fun GetWalletAccountsResponse?.orDefault(
userWalletId: UserWalletId,
accountDTOs: List<WalletAccountDTO>,
userTokensResponse: UserTokensResponse,
): GetWalletAccountsResponse {
if (this != null) return this
return GetWalletAccountsResponse(
wallet = GetWalletAccountsResponse.Wallet(
group = userTokensResponse.group,
sort = userTokensResponse.sort,
totalAccounts = accountDTOs.size,
),
accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = userTokensResponse.tokens),
unassignedTokens = emptyList(),
)
}
}

View file

@ -5,6 +5,7 @@ import arrow.core.some
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.MultiAccountListProducer
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -35,10 +36,11 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
override fun produce(): Flow<List<AccountList>> {
return userWalletsStore.userWallets
.map { it.map(UserWallet::walletId) }
.distinctUntilChanged()
.flatMapLatest { userWallets ->
.flatMapLatest { ids ->
combine(
flows = userWallets.map(walletAccountListFlowFactory::create),
flows = ids.map(walletAccountListFlowFactory::create),
transform = ::listOf,
)
}

View file

@ -2,7 +2,6 @@ package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.none
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -11,16 +10,13 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull
/**
* Default implementation of [SingleAccountListProducer].
* Produces a list of [AccountList] for a specific user wallet.
*
* @property params params containing the user wallet ID
* @property userWalletsStore store that provides user wallets
* @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet
* @property dispatchers coroutine dispatchers provider
*
@ -28,7 +24,6 @@ import kotlinx.coroutines.flow.mapNotNull
*/
internal class DefaultSingleAccountListProducer @AssistedInject constructor(
@Assisted val params: SingleAccountListProducer.Params,
private val userWalletsStore: UserWalletsStore,
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : SingleAccountListProducer {
@ -37,11 +32,7 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
override fun produce(): Flow<AccountList> {
return userWalletsStore.userWallets
.mapNotNull { userWallets ->
userWallets.firstOrNull { it.walletId == params.userWalletId }
}
.flatMapLatest(walletAccountListFlowFactory::create)
return walletAccountListFlowFactory.create(userWalletId = params.userWalletId)
.flowOn(dispatchers.default)
}

View file

@ -4,9 +4,11 @@ import com.tangem.data.account.converter.AccountListConverter
import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.models.wallet.requireColdWallet
import kotlinx.coroutines.flow.*
@ -22,12 +24,15 @@ import javax.inject.Inject
[REDACTED_AUTHOR]
*/
internal class WalletAccountListFlowFactory @Inject constructor(
private val userWalletsStore: UserWalletsStore,
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
private val accountListConverterFactory: AccountListConverter.Factory,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
) {
fun create(userWallet: UserWallet): Flow<AccountList> {
fun create(userWalletId: UserWalletId): Flow<AccountList> {
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
return if (userWallet.isMultiCurrency) {
createForMultiWallet(userWallet)
} else {
@ -53,6 +58,6 @@ internal class WalletAccountListFlowFactory @Inject constructor(
setOf(cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet))
}
return AccountList.empty(userWallet = userWallet, cryptoCurrencies = currencies)
return AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = currencies)
}
}

View file

@ -24,6 +24,7 @@ import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.replaceBy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@ -101,12 +102,33 @@ internal class DefaultAccountsCRUDRepository(
}
override suspend fun saveAccounts(accountList: AccountList) {
val userWalletId = accountList.userWallet.walletId
val converter = convertersContainer.createCryptoPortfolioConverter(userWalletId = accountList.userWalletId)
val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = accountList.userWallet)
val accountsResponse = converter.convert(value = accountList)
val accountDTOs = converter.convertListBack(
input = accountList.accounts.filterIsInstance<Account.CryptoPortfolio>(),
)
walletAccountsSaver.pushAndStore(userWalletId = userWalletId, response = accountsResponse)
val syncedResponse = walletAccountsSaver.push(userWalletId = accountList.userWalletId, accounts = accountDTOs)
if (syncedResponse != null) {
walletAccountsSaver.store(userWalletId = accountList.userWalletId, response = syncedResponse)
}
}
override suspend fun saveAccount(account: Account.CryptoPortfolio) {
val store = getAccountsResponseStore(userWalletId = account.userWalletId)
val converter = convertersContainer.createCryptoPortfolioConverter(userWalletId = account.userWalletId)
val newAccountDTO = converter.convertBack(value = account)
store.updateData { response ->
response ?: return@updateData response
response.copy(
accounts = response.accounts.toMutableList().apply {
replaceBy(newAccountDTO) { it.id == newAccountDTO.id }
},
)
}
}
override suspend fun getTotalAccountsCountSync(userWalletId: UserWalletId): Option<Int> = option {

View file

@ -0,0 +1,68 @@
package com.tangem.data.account.utils
import com.tangem.data.account.converter.CryptoPortfolioConverter
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import javax.inject.Inject
/**
* Factory to create default [GetWalletAccountsResponse].
*
* @property userWalletsListRepository repository to get user wallet information
* @property cryptoPortfolioCF converter factory to convert crypto portfolio accounts
* @property userTokensResponseFactory factory to create [UserTokensResponse]
* @property cardCryptoCurrencyFactory factory to get default coins for multi-currency wallet
*
[REDACTED_AUTHOR]
*/
internal class DefaultWalletAccountsResponseFactory @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory,
private val userTokensResponseFactory: UserTokensResponseFactory,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
) {
suspend fun create(userWalletId: UserWalletId, userTokensResponse: UserTokensResponse?): GetWalletAccountsResponse {
val userWallet = userWalletsListRepository.userWalletsSync().firstOrNull { it.walletId == userWalletId }
val accountDTOs = userWallet?.let(::createDefaultAccountDTOs).orEmpty()
val response = userTokensResponse.orDefault(userWallet = userWallet)
return GetWalletAccountsResponse(
wallet = GetWalletAccountsResponse.Wallet(
group = response.group,
sort = response.sort,
totalAccounts = accountDTOs.size,
),
accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = response.tokens),
unassignedTokens = emptyList(),
)
}
private fun createDefaultAccountDTOs(userWallet: UserWallet): List<WalletAccountDTO> {
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
val converter = cryptoPortfolioCF.create(userWallet = userWallet)
return converter.convertListBack(input = accounts)
}
private fun UserTokensResponse?.orDefault(userWallet: UserWallet?): UserTokensResponse {
if (this != null) return this
return userTokensResponseFactory.createUserTokensResponse(
currencies = userWallet?.let(cardCryptoCurrencyFactory::createDefaultCoinsForMultiCurrencyWallet).orEmpty(),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
}
}

View file

@ -8,7 +8,6 @@ import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
internal fun createWalletAccountDTO(
@ -72,13 +71,13 @@ internal fun createGetWalletAccountsResponse(
}
internal fun createAccountList(
userWallet: UserWallet,
userWalletId: UserWalletId,
sortType: TokensSortType = TokensSortType.BALANCE,
groupType: TokensGroupType = TokensGroupType.NETWORK,
): AccountList {
return AccountList(
userWallet = userWallet,
accounts = setOf(createCryptoPortfolio(userWallet.walletId)),
userWalletId = userWalletId,
accounts = setOf(createCryptoPortfolio(userWalletId)),
totalAccounts = 1,
sortType = sortType,
groupType = groupType,

View file

@ -96,7 +96,7 @@ class AccountListConverterTest {
),
expected = Result.success(
createAccountList(
userWallet = userWallet,
userWalletId = userWallet.walletId,
sortType = TokensSortType.BALANCE,
groupType = TokensGroupType.NETWORK,
),
@ -110,7 +110,7 @@ class AccountListConverterTest {
),
expected = Result.success(
createAccountList(
userWallet = userWallet,
userWalletId = userWallet.walletId,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),
@ -124,7 +124,7 @@ class AccountListConverterTest {
),
expected = Result.success(
createAccountList(
userWallet = userWallet,
userWalletId = userWallet.walletId,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),

View file

@ -46,7 +46,7 @@ class GetWalletAccountsResponseConverterTest {
@Test
fun `cryptoPortfolioConverter throws exception`() {
// Arrange
val domain = createAccountList(userWallet = userWallet)
val domain = createAccountList(userWalletId = userWallet.walletId)
val exception = IllegalStateException("Test exception")
every { cryptoPortfolioConverter.convertBack(any()) } throws exception
@ -92,7 +92,7 @@ class GetWalletAccountsResponseConverterTest {
return listOf(
ConvertModel(
value = createAccountList(
userWallet = userWallet,
userWalletId = userWallet.walletId,
sortType = TokensSortType.BALANCE,
groupType = TokensGroupType.NETWORK,
),
@ -106,7 +106,7 @@ class GetWalletAccountsResponseConverterTest {
),
ConvertModel(
value = createAccountList(
userWallet = userWallet,
userWalletId = userWallet.walletId,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
),

View file

@ -2,14 +2,10 @@ package com.tangem.data.account.converter
import com.google.common.truth.Truth
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@ -19,13 +15,11 @@ class SaveWalletAccountsResponseConverterTest {
@Test
fun convert() {
// Arrange
val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns UserWalletId("011")
}
val userWalletId = UserWalletId("011")
val accountList = AccountList(
userWallet = userWallet,
accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)),
userWalletId = userWalletId,
accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)),
totalAccounts = 1,
)
.getOrNull()!!
@ -36,7 +30,7 @@ class SaveWalletAccountsResponseConverterTest {
// Assert
val expected = SaveWalletAccountsResponse(
accounts = listOf(
WalletAccountDTO(
SaveWalletAccountsResponse.AccountDTO(
id = accountList.mainAccount.accountId.value,
name = (accountList.mainAccount.accountName as? AccountName.Custom)?.value,
derivationIndex = accountList.mainAccount.derivationIndex.value,

View file

@ -5,6 +5,7 @@ import com.tangem.data.account.converter.createGetWalletAccountsResponse
import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponse
@ -36,6 +37,7 @@ class DefaultWalletAccountsFetcherTest {
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
private val fetchWalletAccountsErrorHandler: FetchWalletAccountsErrorHandler = mockk(relaxUnitFun = true)
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true)
private val fetcher: DefaultWalletAccountsFetcher = DefaultWalletAccountsFetcher(
@ -43,6 +45,7 @@ class DefaultWalletAccountsFetcherTest {
accountsResponseStoreFactory = accountsResponseStoreFactory,
userTokensSaver = userTokensSaver,
fetchWalletAccountsErrorHandler = fetchWalletAccountsErrorHandler,
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
eTagsStore = eTagsStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@ -114,7 +117,7 @@ class DefaultWalletAccountsFetcherTest {
eTag = eTag,
body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts),
)
} returns ApiResponse.Success(data = Unit)
} returns ApiResponse.Success(data = updatedAccountsResponse)
// Act
fetcher.fetch(userWalletId)
@ -205,6 +208,16 @@ class DefaultWalletAccountsFetcherTest {
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
} returns apiError as ApiResponse<GetWalletAccountsResponse>
coEvery {
fetchWalletAccountsErrorHandler.handle(
error = apiError.cause,
userWalletId = userWalletId,
savedAccountsResponse = null,
pushWalletAccounts = any(),
storeWalletAccounts = any(),
)
} returns savedAccountsResponse
// Act
fetcher.fetch(userWalletId)
@ -260,26 +273,26 @@ class DefaultWalletAccountsFetcherTest {
@Test
fun `push should call saveWalletAccounts with correct params`() = runTest {
// Arrange
val accounts = listOf(createWalletAccountDTO(userWalletId = userWalletId, tokens = null))
val response = SaveWalletAccountsResponse(accounts)
val getResponse = createGetWalletAccountsResponse(userWalletId, tokens = null)
val saveResponse = SaveWalletAccountsResponse(getResponse.accounts)
coEvery {
tangemTechApi.saveWalletAccounts(
walletId = userWalletId.stringValue,
eTag = eTag,
body = response,
body = saveResponse,
)
} returns ApiResponse.Success(data = Unit)
} returns ApiResponse.Success(data = getResponse)
// Act
fetcher.push(userWalletId, response)
fetcher.push(userWalletId, saveResponse)
// Assert
coVerify {
tangemTechApi.saveWalletAccounts(
walletId = userWalletId.stringValue,
eTag = eTag,
body = response,
body = saveResponse,
)
}
}
@ -303,7 +316,7 @@ class DefaultWalletAccountsFetcherTest {
eTag = eTag,
body = response,
)
} returns saveApiResponse as ApiResponse<Unit>
} returns saveApiResponse as ApiResponse<GetWalletAccountsResponse>
// Act
val actual = runCatching { fetcher.push(userWalletId, response) }.exceptionOrNull()!!
@ -313,42 +326,6 @@ class DefaultWalletAccountsFetcherTest {
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class PushAndStore {
@Test
fun `pushAndStore should call push and store with correct params`() = runTest {
// Arrange
val accounts = listOf(createWalletAccountDTO(userWalletId = userWalletId, tokens = null))
val response = createGetWalletAccountsResponse(userWalletId).copy(accounts = accounts)
coEvery {
tangemTechApi.saveWalletAccounts(
walletId = userWalletId.stringValue,
eTag = eTag,
body = SaveWalletAccountsResponse(accounts = response.accounts),
)
} returns ApiResponse.Success(data = Unit)
coEvery { accountsResponseStore.updateData(any()) } returns mockk()
// Act
fetcher.pushAndStore(userWalletId, response)
// Assert
coVerifyOrder {
tangemTechApi.saveWalletAccounts(
walletId = userWalletId.stringValue,
eTag = eTag,
body = SaveWalletAccountsResponse(accounts = response.accounts),
)
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.updateData(any())
}
}
}
private fun createToken(
networkId: String = "ethereum",
derivationPath: String = "m/44'/60'/0'/0/0",

View file

@ -1,9 +1,9 @@
package com.tangem.data.account.fetcher
import com.tangem.data.account.converter.CryptoPortfolioConverter
import com.tangem.data.account.converter.createGetWalletAccountsResponse
import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
@ -11,12 +11,11 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.*
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@ -29,35 +28,25 @@ import org.junit.jupiter.api.TestInstance
class FetchWalletAccountsErrorHandlerTest {
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
private val userWalletsStore: UserWalletsStore = mockk()
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory = mockk()
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
private val userTokensResponseFactory: UserTokensResponseFactory = mockk()
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
private val handler = FetchWalletAccountsErrorHandler(
userTokensSaver = userTokensSaver,
userWalletsStore = userWalletsStore,
userTokensResponseStore = userTokensResponseStore,
cryptoPortfolioCF = cryptoPortfolioCF,
userTokensResponseFactory = userTokensResponseFactory,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
)
private val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
}
private val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse =
mockk(relaxed = true)
private val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
@BeforeEach
fun setupEach() {
clearMocks(
userTokensSaver,
userWalletsStore,
userTokensResponseStore,
cryptoPortfolioCF,
cryptoPortfolioConverter,
cardCryptoCurrencyFactory,
defaultWalletAccountsResponseFactory,
)
}
@ -70,9 +59,6 @@ class FetchWalletAccountsErrorHandlerTest {
errorBody = null,
)
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk()
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk()
// Act
handler.handle(
error = error,
@ -84,12 +70,8 @@ class FetchWalletAccountsErrorHandlerTest {
// Assert
coVerify(inverse = true) {
userWalletsStore.getSyncStrict(key = any())
userTokensResponseStore.getSyncOrNull(userWalletId = any())
userTokensResponseFactory.createUserTokensResponse(any(), any(), any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
cryptoPortfolioCF.create(any())
cryptoPortfolioConverter.convertListBack(any())
defaultWalletAccountsResponseFactory.create(userWalletId = any(), userTokensResponse = any())
pushWalletAccounts(any(), any())
userTokensSaver.push(userWalletId = any(), response = any())
storeWalletAccounts(any(), any())
@ -105,29 +87,11 @@ class FetchWalletAccountsErrorHandlerTest {
errorBody = null,
)
val accountDTO = WalletAccountDTO(
id = "nibh",
name = "Michael Dotson",
derivationIndex = 7135,
icon = "consectetuer",
iconColor = "ferri",
tokens = listOf(),
totalTokens = 7738,
totalNetworks = 3348,
)
val accountDTO = createWalletAccountDTO(userWalletId)
val savedAccountsResponse = GetWalletAccountsResponse(
wallet = GetWalletAccountsResponse.Wallet(
group = UserTokensResponse.GroupType.NONE,
sort = UserTokensResponse.SortType.MANUAL,
totalAccounts = 1,
),
accounts = listOf(accountDTO),
unassignedTokens = emptyList(),
)
val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk(relaxed = true)
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse
// Act
handler.handle(
@ -140,18 +104,14 @@ class FetchWalletAccountsErrorHandlerTest {
// Assert
coVerify {
pushWalletAccounts(userWalletId, listOf(accountDTO))
userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
pushWalletAccounts(userWalletId, listOf(accountDTO))
storeWalletAccounts(userWalletId, savedAccountsResponse)
}
coVerify(inverse = true) {
userWalletsStore.getSyncStrict(key = any())
userTokensResponseStore.getSyncOrNull(userWalletId = any())
userTokensResponseFactory.createUserTokensResponse(any(), any(), any())
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
cryptoPortfolioCF.create(any())
cryptoPortfolioConverter.convertListBack(any())
defaultWalletAccountsResponseFactory.create(userWalletId = any(), userTokensResponse = any())
}
}
@ -160,9 +120,6 @@ class FetchWalletAccountsErrorHandlerTest {
// Arrange
val error = ApiResponseError.TimeoutException()
val accounts = AccountList.empty(userWallet).accounts
.filterIsInstance<Account.CryptoPortfolio>()
val accountDTO = WalletAccountDTO(
id = "nibh",
name = "Michael Dotson",
@ -186,21 +143,10 @@ class FetchWalletAccountsErrorHandlerTest {
val userTokensResponse = savedAccountsResponse.toUserTokensResponse()
every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet
every { cryptoPortfolioCF.create(userWallet) } returns cryptoPortfolioConverter
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountDTO)
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId) } returns null
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
} returns userTokensResponse
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList()
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk(relaxed = true)
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId) } returns userTokensResponse
coEvery {
defaultWalletAccountsResponseFactory.create(userWalletId, userTokensResponse)
} returns savedAccountsResponse
// Act
handler.handle(
@ -213,16 +159,8 @@ class FetchWalletAccountsErrorHandlerTest {
// Assert
coVerify {
userWalletsStore.getSyncStrict(userWalletId)
cryptoPortfolioCF.create(userWallet)
cryptoPortfolioConverter.convertListBack(accounts)
userTokensResponseStore.getSyncOrNull(userWalletId)
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet)
defaultWalletAccountsResponseFactory.create(userWalletId, userTokensResponse)
storeWalletAccounts(userWalletId, any())
}

View file

@ -51,8 +51,8 @@ class DefaultMultiAccountListProducerTest {
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList)
val accountList = AccountList.empty(userWalletId)
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -63,7 +63,7 @@ class DefaultMultiAccountListProducerTest {
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
}
}
@ -73,11 +73,11 @@ class DefaultMultiAccountListProducerTest {
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE)
val accountList = AccountList.empty(userWalletId)
val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -95,9 +95,9 @@ class DefaultMultiAccountListProducerTest {
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
}
}
@ -107,10 +107,10 @@ class DefaultMultiAccountListProducerTest {
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
val accountList = AccountList.empty(userWalletId)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -128,9 +128,9 @@ class DefaultMultiAccountListProducerTest {
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
}
}
@ -141,7 +141,7 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsStore.userWallets } returns userWalletsFlow
val exception = RuntimeException("Converter error")
every { walletAccountListFlowFactory.create(userWallet) } throws exception
every { walletAccountListFlowFactory.create(userWalletId) } throws exception
// Act
val actual = producer.produceWithFallback().let(::getEmittedValues)
@ -152,7 +152,7 @@ class DefaultMultiAccountListProducerTest {
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
}
}
@ -178,7 +178,7 @@ class DefaultMultiAccountListProducerTest {
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
every { walletAccountListFlowFactory.create(userWallet) } returns emptyFlow()
every { walletAccountListFlowFactory.create(userWalletId) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -188,7 +188,7 @@ class DefaultMultiAccountListProducerTest {
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
}
}
@ -203,9 +203,9 @@ class DefaultMultiAccountListProducerTest {
val userWalletsFlow = MutableStateFlow(listOf(userWallet, userWallet2))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList)
every { walletAccountListFlowFactory.create(userWallet2) } returns emptyFlow()
val accountList = AccountList.empty(userWalletId)
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
every { walletAccountListFlowFactory.create(userWalletId2) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -215,8 +215,8 @@ class DefaultMultiAccountListProducerTest {
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWallet2)
walletAccountListFlowFactory.create(userWalletId)
walletAccountListFlowFactory.create(userWalletId2)
}
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.data.account.producer
import com.google.common.truth.Truth
import com.tangem.common.test.utils.getEmittedValues
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.models.TokensSortType
@ -11,7 +10,6 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@ -26,7 +24,6 @@ import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultSingleAccountListProducerTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk()
private val userWalletId = UserWalletId("011")
@ -36,24 +33,22 @@ class DefaultSingleAccountListProducerTest {
private val producer = DefaultSingleAccountListProducer(
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
userWalletsStore = userWalletsStore,
walletAccountListFlowFactory = walletAccountListFlowFactory,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@AfterEach
fun tearDownEach() {
clearMocks(userWalletsStore, walletAccountListFlowFactory)
clearMocks(walletAccountListFlowFactory)
}
@Test
fun produce() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
MutableStateFlow(listOf(userWallet))
val accountList = AccountList.empty(userWallet)
every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList)
val accountList = AccountList.empty(userWalletId)
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -63,22 +58,18 @@ class DefaultSingleAccountListProducerTest {
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
}
}
@Test
fun `flow will updated if factoryFlow is updated`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE)
val accountList = AccountList.empty(userWalletId)
val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -95,23 +86,18 @@ class DefaultSingleAccountListProducerTest {
Truth.assertThat(secondEmission).containsExactly(updatedAccountList)
coVerifyOrder {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
walletAccountListFlowFactory.create(userWalletId)
}
}
@Test
fun `flow is filtered the same response`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWallet)
val accountList = AccountList.empty(userWalletId)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull()
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -128,71 +114,8 @@ class DefaultSingleAccountListProducerTest {
Truth.assertThat(secondEmission).containsExactly(accountList)
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
walletAccountListFlowFactory.create(userWalletId)
walletAccountListFlowFactory.create(userWalletId)
}
}
@Test
fun `flow is empty if factory throws exception`() = runTest {
// Arrange
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
val exception = RuntimeException("Converter error")
every { walletAccountListFlowFactory.create(userWallet) } throws exception
// Act
val actual = producer.produceWithFallback().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty() // no emissions
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
walletAccountListFlowFactory.create(userWallet)
}
}
@Test
fun `flow is empty if userWalletsFlow returns empty flow`() = runTest {
// Arrange
val userWalletsFlow = emptyFlow<List<UserWallet>>()
every { userWalletsStore.userWallets } returns userWalletsFlow
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty() // no emissions
coVerify(exactly = 1) { userWalletsStore.userWallets }
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
}
@Test
fun `flow is empty if userWalletsFlow doesn't contains userWalletId from params`() = runTest {
// Arrange
val unknownId = UserWalletId("012")
val unknownWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns unknownId
}
val userWalletsFlow = MutableStateFlow(listOf(unknownWallet))
every { userWalletsStore.userWallets } returns userWalletsFlow
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty() // no emissions
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.userWallets
}
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -28,6 +29,7 @@ import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class WalletAccountListFlowFactoryTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk()
private val accountsResponseStore: AccountsResponseStore = mockk()
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
@ -38,6 +40,7 @@ class WalletAccountListFlowFactoryTest {
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val factory = WalletAccountListFlowFactory(
userWalletsStore = userWalletsStore,
accountsResponseStoreFactory = accountsResponseStoreFactory,
accountListConverterFactory = accountListConverterFactory,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
@ -49,6 +52,7 @@ class WalletAccountListFlowFactoryTest {
@AfterEach
fun tearDownEach() {
clearMocks(
userWalletsStore,
accountsResponseStoreFactory,
accountsResponseStore,
accountListConverterFactory,
@ -66,17 +70,19 @@ class WalletAccountListFlowFactoryTest {
every { this@mockk.isMultiCurrency } returns true
}
every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet
val accountsResponse = createGetWalletAccountsResponse(userWalletId)
every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore
every { accountsResponseStore.data } returns accountsResponseStoreFlow
accountsResponseStoreFlow.value = accountsResponse
val accountList = AccountList.empty(userWallet)
val accountList = AccountList.empty(userWalletId)
every { accountListConverterFactory.create(userWallet) } returns accountListConverter
every { accountListConverter.convert(accountsResponse) } returns accountList
// Act
val actual = factory.create(userWallet).let(::getEmittedValues)
val actual = factory.create(userWalletId).let(::getEmittedValues)
// Assert
val expected = accountList
@ -99,14 +105,16 @@ class WalletAccountListFlowFactoryTest {
fun `create for single wallet`() = runTest {
val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false)
every { userWalletsStore.getSyncStrict(userWallet.walletId) } returns userWallet
val currency = cryptoCurrencyFactory.ethereum
every { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } returns currency
// Act
val actual = factory.create(userWallet).let(::getEmittedValues)
val actual = factory.create(userWallet.walletId).let(::getEmittedValues)
// Assert
val expected = AccountList.empty(userWallet = userWallet, cryptoCurrencies = setOf(currency))
val expected = AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = setOf(currency))
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
@ -126,16 +134,18 @@ class WalletAccountListFlowFactoryTest {
fun `flow is created for single wallet with token`() = runTest {
val nodl = MockUserWalletFactory.createSingleWalletWithToken()
every { userWalletsStore.getSyncStrict(nodl.walletId) } returns nodl
val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet()
every {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl)
} returns currencies.toList()
// Act
val actual = factory.create(nodl).let(::getEmittedValues)
val actual = factory.create(nodl.walletId).let(::getEmittedValues)
// Assert
val expected = AccountList.empty(userWallet = nodl, cryptoCurrencies = currencies)
val expected = AccountList.empty(userWalletId = nodl.walletId, cryptoCurrencies = currencies)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {

View file

@ -25,7 +25,6 @@ import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
@ -37,6 +36,7 @@ import kotlin.time.Duration.Companion.minutes
/**
[REDACTED_AUTHOR]
*/
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultAccountsCRUDRepositoryTest {
@ -580,22 +580,18 @@ class DefaultAccountsCRUDRepositoryTest {
@Test
fun `saveAccounts should call API and update store`() = runTest {
// Arrange
val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
}
val accountList = AccountList.empty(userWalletId = userWalletId)
val accounts = accountList.accounts.filterIsInstance<CryptoPortfolio>()
val accountList = AccountList.empty(userWallet = userWallet)
val accountsResponse = mockk<GetWalletAccountsResponse>()
val accountsResponse = createGetWalletAccountsResponse(userWalletId)
accountsResponseStoreFlow.value = accountsResponse
val converter = mockk<GetWalletAccountsResponseConverter> {
every { this@mockk.convert(accountList) } returns accountsResponse
val converter = mockk<CryptoPortfolioConverter> {
every { this@mockk.convertListBack(accounts) } returns accountsResponse.accounts
}
every {
convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet)
} returns converter
every { convertersContainer.createCryptoPortfolioConverter(userWalletId) } returns converter
coEvery { walletAccountsSaver.push(userWalletId, accountsResponse.accounts) } returns accountsResponse
// Act
repository.saveAccounts(accountList)
@ -604,35 +600,30 @@ class DefaultAccountsCRUDRepositoryTest {
Truth.assertThat(accountsResponseStoreFlow.value).isEqualTo(accountsResponse)
coVerifyOrder {
convertersContainer.getWalletAccountsResponseCF.create(userWallet)
converter.convert(accountList)
walletAccountsSaver.pushAndStore(userWalletId, accountsResponse)
convertersContainer.createCryptoPortfolioConverter(userWalletId)
converter.convertListBack(accounts)
walletAccountsSaver.push(userWalletId, accountsResponse.accounts)
}
}
@Test
fun `saveAccounts if API request is failed`() = runTest {
// Arrange
val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
}
val accountList = AccountList.empty(userWalletId = userWalletId)
val accounts = accountList.accounts.filterIsInstance<CryptoPortfolio>()
val accountList = AccountList.empty(userWallet = userWallet)
val accountsResponse = mockk<GetWalletAccountsResponse>()
val accountsResponse = createGetWalletAccountsResponse(userWalletId)
accountsResponseStoreFlow.value = accountsResponse
val converter = mockk<GetWalletAccountsResponseConverter> {
every { this@mockk.convert(accountList) } returns accountsResponse
val converter = mockk<CryptoPortfolioConverter> {
every { this@mockk.convertListBack(accounts) } returns accountsResponse.accounts
}
every {
convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet)
} returns converter
every { convertersContainer.createCryptoPortfolioConverter(userWalletId) } returns converter
val exception = Exception("Test error")
coEvery { walletAccountsSaver.pushAndStore(userWalletId, accountsResponse) } throws exception
coEvery { walletAccountsSaver.push(userWalletId, accountsResponse.accounts) } throws exception
// Act
val actual = runCatching { repository.saveAccounts(accountList) }.exceptionOrNull()!!
@ -642,9 +633,8 @@ class DefaultAccountsCRUDRepositoryTest {
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
coVerifyOrder {
convertersContainer.getWalletAccountsResponseCF.create(userWallet)
converter.convert(accountList)
walletAccountsSaver.pushAndStore(userWalletId, accountsResponse)
convertersContainer.createCryptoPortfolioConverter(userWalletId)
converter.convertListBack(accounts)
}
}
}

View file

@ -0,0 +1,240 @@
package com.tangem.data.account.utils
import com.google.common.truth.Truth
import com.tangem.data.account.converter.CryptoPortfolioConverter
import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultWalletAccountsResponseFactoryTest {
private val userWalletsListRepository = mockk<UserWalletsListRepository>()
private val cryptoPortfolioCF = mockk<CryptoPortfolioConverter.Factory>()
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
private val userTokensResponseFactory = mockk<UserTokensResponseFactory>()
private val cardCryptoCurrencyFactory = mockk<CardCryptoCurrencyFactory>()
private val factory = DefaultWalletAccountsResponseFactory(
userWalletsListRepository = userWalletsListRepository,
cryptoPortfolioCF = cryptoPortfolioCF,
userTokensResponseFactory = userTokensResponseFactory,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
)
private val userWalletId = UserWalletId("011")
@BeforeEach
fun setUpEach() {
every { cryptoPortfolioCF.create(any()) } returns cryptoPortfolioConverter
}
@AfterEach
fun tearDownEach() {
clearMocks(
userWalletsListRepository,
cryptoPortfolioCF,
cryptoPortfolioConverter,
userTokensResponseFactory,
cardCryptoCurrencyFactory,
)
}
@Test
fun `create returns empty accounts when user wallet not found`() = runTest {
// Arrange
val userTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
coEvery { userWalletsListRepository.userWalletsSync() } returns emptyList()
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
} returns userTokensResponse
// Act
val actual = factory.create(userWalletId = userWalletId, userTokensResponse = null)
// Assert
val expected = GetWalletAccountsResponse(
wallet = GetWalletAccountsResponse.Wallet(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
totalAccounts = 0,
),
accounts = emptyList(),
unassignedTokens = emptyList(),
)
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
userWalletsListRepository.userWalletsSync()
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
}
}
@Test
fun `create returns response with default tokens when userTokensResponse is null`() = runTest {
// Arrange
val userWallet = mockk<UserWallet>(relaxed = true) {
every { walletId } returns userWalletId
}
val defaultCoins = listOf(mockk<CryptoCurrency.Coin>())
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns defaultCoins
val defaultResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = listOf(mockk(relaxed = true)),
)
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = defaultCoins,
isGroupedByNetwork = false,
isSortedByBalance = false,
)
} returns defaultResponse
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
val accountsDTO = createWalletAccountDTO(userWalletId)
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO)
// Act
val actual = factory.create(userWalletId, null)
// Assert
val expected = GetWalletAccountsResponse(
wallet = GetWalletAccountsResponse.Wallet(
group = defaultResponse.group,
sort = defaultResponse.sort,
totalAccounts = 1,
),
accounts = listOf(accountsDTO),
unassignedTokens = emptyList(),
)
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
userWalletsListRepository.userWalletsSync()
cryptoPortfolioConverter.convertListBack(accounts)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet)
userTokensResponseFactory.createUserTokensResponse(
currencies = defaultCoins,
isGroupedByNetwork = false,
isSortedByBalance = false,
)
}
}
@Test
fun `create returns response with default tokens when userTokensResponse is null and no default coins`() = runTest {
// Arrange
val userWallet = mockk<UserWallet>(relaxed = true) {
every { walletId } returns userWalletId
}
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList()
val defaultResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
} returns defaultResponse
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
every { cryptoPortfolioConverter.convertListBack(accounts) } returns emptyList()
// Act
val actual = factory.create(userWalletId, null)
// Assert
val expected = GetWalletAccountsResponse(
wallet = GetWalletAccountsResponse.Wallet(
group = defaultResponse.group,
sort = defaultResponse.sort,
totalAccounts = 0,
),
accounts = emptyList(),
unassignedTokens = emptyList(),
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `create returns response with assigned tokens`() = runTest {
// Arrange
val userWallet = mockk<UserWallet>(relaxed = true) {
every { walletId } returns userWalletId
}
val assignedTokens = listOf(mockk<CryptoCurrency.Token>(), mockk<CryptoCurrency.Token>())
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
val userTokensResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = listOf(mockk(relaxed = true)),
)
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = assignedTokens,
isGroupedByNetwork = false,
isSortedByBalance = false,
)
} returns userTokensResponse
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
val accountsDTO = createWalletAccountDTO(userWalletId)
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO)
// Act
val actual = factory.create(userWalletId, userTokensResponse)
// Assert
val expected = GetWalletAccountsResponse(
wallet = GetWalletAccountsResponse.Wallet(
group = userTokensResponse.group,
sort = userTokensResponse.sort,
totalAccounts = 1,
),
accounts = listOf(accountsDTO),
unassignedTokens = emptyList(),
)
Truth.assertThat(actual).isEqualTo(expected)
}
}

View file

@ -12,18 +12,14 @@ import com.tangem.domain.models.wallet.UserWalletId
*/
interface WalletAccountsSaver {
/** Push and store wallet accounts [response] by [userWalletId] */
@Throws
suspend fun pushAndStore(userWalletId: UserWalletId, response: GetWalletAccountsResponse)
/** Store wallet accounts [response] by [userWalletId] */
suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse)
/** Push wallet accounts [body] by [userWalletId] */
@Throws
suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse)
suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse): GetWalletAccountsResponse?
/** Push wallet accounts [accounts] by [userWalletId] */
@Throws
suspend fun push(userWalletId: UserWalletId, accounts: List<WalletAccountDTO>)
suspend fun push(userWalletId: UserWalletId, accounts: List<WalletAccountDTO>): GetWalletAccountsResponse?
}

View file

@ -326,6 +326,8 @@ class NetworkFactory @Inject constructor(
Blockchain.Pepecoin, Blockchain.PepecoinTestnet,
Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet,
Blockchain.Quai, Blockchain.QuaiTestnet,
Blockchain.Linea, Blockchain.LineaTestnet,
Blockchain.ArbitrumNova,
-> Network.TransactionExtrasType.NONE
// endregion
}

View file

@ -8,6 +8,7 @@ import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.repository.DefaultNetworksRepository
import com.tangem.data.networks.store.DefaultNetworksStatusesStore
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.data.networks.utils.DefaultNetworksCleaner
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.network.entity.NetworkStatusDM
@ -15,6 +16,7 @@ import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.datasource.utils.setTypes
import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.networks.utils.NetworksCleaner
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -67,4 +69,18 @@ internal object NetworkDataModule {
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideNetworksCleaner(
networksStatusesStore: NetworksStatusesStore,
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
): NetworksCleaner {
return DefaultNetworksCleaner(
networksStatusesStore = networksStatusesStore,
walletManagersFacade = walletManagersFacade,
dispatchers = dispatchers,
)
}
}

View file

@ -96,6 +96,20 @@ internal class DefaultNetworksStatusesStore(
}
}
override suspend fun clear(userWalletId: UserWalletId, networks: Set<Network>) {
persistenceDataStore.updateData { storedStatuses ->
storedStatuses.toMutableMap().apply {
val updatedValues = this[userWalletId.stringValue].orEmpty().filterNot {
networks.any { network ->
it.networkId.value == network.rawId && it.derivationPath.value == network.derivationPath.value
}
}
this[userWalletId.stringValue] = updatedValues.toSet()
}
}
}
private suspend fun updateInRuntime(
userWalletId: UserWalletId,
networks: Set<Network>,

View file

@ -41,4 +41,7 @@ internal interface NetworksStatusesStore {
* See complex methods in `NetworksStatusesStoreExt`.
*/
suspend fun store(userWalletId: UserWalletId, status: NetworkStatus)
/** Clear statuses of [networks] by [userWalletId] */
suspend fun clear(userWalletId: UserWalletId, networks: Set<Network>)
}

View file

@ -0,0 +1,73 @@
package com.tangem.data.networks.utils
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.utils.NetworksCleaner
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Default implementation of [NetworksCleaner].
*
* @property networksStatusesStore Store to manage network statuses.
* @property walletManagersFacade Facade to manage wallet managers.
* @property dispatchers Coroutine dispatchers provider.
*
[REDACTED_AUTHOR]
*/
internal class DefaultNetworksCleaner(
private val networksStatusesStore: NetworksStatusesStore,
private val walletManagersFacade: WalletManagersFacade,
private val dispatchers: CoroutineDispatcherProvider,
) : NetworksCleaner {
override suspend fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
withContext(dispatchers.default) {
val (networks, tokens) = currencies.partitionByType()
coroutineScope {
launch { cleanStore(userWalletId = userWalletId, networks = networks) }
launch { cleanWalletManager(userWalletId = userWalletId, networks = networks, tokens = tokens) }
}
}
}
private suspend fun cleanStore(userWalletId: UserWalletId, networks: Set<Network>) {
if (networks.isNotEmpty()) {
networksStatusesStore.clear(userWalletId = userWalletId, networks = networks)
}
}
private suspend fun cleanWalletManager(
userWalletId: UserWalletId,
networks: Set<Network>,
tokens: Set<CryptoCurrency.Token>,
) {
if (networks.isNotEmpty()) {
walletManagersFacade.remove(userWalletId = userWalletId, networks = networks)
}
if (tokens.isNotEmpty()) {
walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = tokens)
}
}
private fun List<CryptoCurrency>.partitionByType(): Pair<Set<Network>, Set<CryptoCurrency.Token>> {
val networks = mutableSetOf<Network>()
val tokens = mutableSetOf<CryptoCurrency.Token>()
for (currency in this) {
when (currency) {
is CryptoCurrency.Coin -> networks.add(currency.network)
is CryptoCurrency.Token -> tokens.add(currency)
}
}
return Pair(networks, tokens)
}
}

View file

@ -0,0 +1,101 @@
package com.tangem.data.networks.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultNetworksCleanerTest {
private val networksStatusesStore = mockk<NetworksStatusesStore>(relaxed = true)
private val walletManagersFacade = mockk<WalletManagersFacade>(relaxed = true)
private val cleaner = DefaultNetworksCleaner(
networksStatusesStore = networksStatusesStore,
walletManagersFacade = walletManagersFacade,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val userWalletId = UserWalletId("011")
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val network = cryptoCurrencyFactory.ethereum.network
private val coin = cryptoCurrencyFactory.ethereum
private val token = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
@BeforeEach
fun setUp() {
clearMocks(networksStatusesStore, walletManagersFacade)
}
@Test
fun `should clear networks and remove managers and tokens when called`() = runTest {
// Arrange
val currencies = listOf(coin, token)
// Act
cleaner(userWalletId = userWalletId, currencies = currencies)
// Assert
coVerifyOrder {
networksStatusesStore.clear(userWalletId, setOf(network))
walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network))
walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = setOf(token))
}
}
@Test
fun `should handle empty currencies`() = runTest {
// Act
cleaner(userWalletId = userWalletId, currencies = emptyList())
// Assert
coVerifyOrder(inverse = true) {
networksStatusesStore.clear(userWalletId = any(), networks = any())
walletManagersFacade.remove(userWalletId = any(), networks = any())
walletManagersFacade.removeTokens(userWalletId = any(), tokens = any())
}
}
@Test
fun `should clear only networks when there are no tokens`() = runTest {
val currencies = listOf(coin)
cleaner(userWalletId = userWalletId, currencies = currencies)
coVerifyOrder {
networksStatusesStore.clear(userWalletId, setOf(network))
walletManagersFacade.remove(userWalletId = userWalletId, networks = setOf(network))
}
coVerifyOrder(inverse = true) {
walletManagersFacade.removeTokens(userWalletId = any(), tokens = any())
}
}
@Test
fun `should clear only tokens when there are no networks`() = runTest {
// Arrange
val currencies = listOf(token)
// Act
cleaner(userWalletId = userWalletId, currencies = currencies)
// Assert
coVerifyOrder {
walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = setOf(token))
}
coVerifyOrder(inverse = true) {
networksStatusesStore.clear(userWalletId = any(), networks = any())
walletManagersFacade.remove(userWalletId = any(), networks = any())
}
}
}

View file

@ -160,5 +160,7 @@ public val Blockchain.mercuryoNetwork: String?
Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> null
Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> null
Blockchain.Quai, Blockchain.QuaiTestnet -> null
Blockchain.Linea, Blockchain.LineaTestnet -> null
Blockchain.ArbitrumNova -> null
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.models.GB_COUNTRY
@ -91,11 +92,16 @@ internal class DefaultSettingsRepository(
}
override suspend fun shouldSaveAccessCodes(): Boolean {
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, default = false)
return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY)?.not()
?: appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY,
default = false,
)
}
override suspend fun setShouldSaveAccessCodes(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, value = value)
appPreferencesStore.store(key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, value = value.not())
}
override suspend fun incrementAppLaunchCounter() {

View file

@ -10,6 +10,7 @@ import com.tangem.data.staking.DefaultStakingTransactionHashRepository
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles
import com.tangem.data.staking.utils.DefaultStakingCleaner
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
import com.tangem.datasource.di.NetworkMoshi
@ -21,6 +22,7 @@ import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.staking.utils.StakingCleaner
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -102,4 +104,16 @@ internal object StakingDataModule {
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles {
return DefaultStakingFeatureToggles(featureTogglesManager)
}
@Provides
@Singleton
fun provideStakingCleaner(
yieldsBalancesStore: YieldsBalancesStore,
dispatchers: CoroutineDispatcherProvider,
): StakingCleaner {
return DefaultStakingCleaner(
yieldsBalancesStore = yieldsBalancesStore,
dispatchers = dispatchers,
)
}
}

View file

@ -96,6 +96,16 @@ internal class DefaultYieldsBalancesStore(
)
}
override suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
persistenceStore.updateData { current ->
current.toMutableMap().apply {
this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty()
.filterNot { it.getStakingId() in stakingIds }
.toSet()
}
}
}
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<YieldBalanceWrapperDTO>) {
val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values)
.filterNotNull()

View file

@ -33,4 +33,7 @@ interface YieldsBalancesStore {
/** Store error by [userWalletId] and [stakingIds] */
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
/** Clear balances of [stakingIds] by [userWalletId] */
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
}

View file

@ -0,0 +1,29 @@
package com.tangem.data.staking.utils
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.utils.StakingCleaner
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Default implementation of [StakingCleaner].
*
* @property yieldsBalancesStore Store to manage yields balances.
* @property dispatchers Coroutine dispatchers provider.
*
[REDACTED_AUTHOR]
*/
internal class DefaultStakingCleaner(
private val yieldsBalancesStore: YieldsBalancesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : StakingCleaner {
override suspend fun invoke(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
if (stakingIds.isEmpty()) return
with(dispatchers.default) {
yieldsBalancesStore.clear(userWalletId, stakingIds)
}
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.data.staking.utils
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultStakingCleanerTest {
private val yieldsBalancesStore = mockk<YieldsBalancesStore>(relaxed = true)
private val cleaner = DefaultStakingCleaner(
yieldsBalancesStore = yieldsBalancesStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val userWalletId = UserWalletId("011")
private val stakingIds = setOf(
StakingID(integrationId = StakingIntegrationID.Coin.Cardano.value, address = "0x1"),
)
@BeforeEach
fun setUp() {
clearMocks(yieldsBalancesStore)
}
@Test
fun `should clear yields balances when called`() = runTest {
// Act
cleaner(userWalletId = userWalletId, stakingIds = stakingIds)
// Assert
coVerifyOrder {
yieldsBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds)
}
}
@Test
fun `should handle empty stakingIds`() = runTest {
// Act
cleaner(userWalletId = userWalletId, stakingIds = emptySet())
// Assert
coVerifyOrder(inverse = true) {
yieldsBalancesStore.clear(userWalletId = any(), stakingIds = any())
}
}
}

View file

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

View file

@ -18,6 +18,7 @@ import kotlinx.coroutines.withContext
import javax.inject.Inject
private const val VALID_STATUS = "valid"
private const val APPROVED_KYC_STATUS = "APPROVED"
private const val TAG = "TangemPay: OnboardingRepository"
internal class DefaultOnboardingRepository @Inject constructor(
@ -47,13 +48,36 @@ internal class DefaultOnboardingRepository @Inject constructor(
override suspend fun getMainScreenCustomerInfo(): Either<UniversalError, MainScreenCustomerInfo> {
return requestHelper.runWithErrorLogs(TAG) {
val result = requestHelper.requestWithPersistedToken { authHeader ->
tangemPayApi.getCustomerMe(authHeader)
}.result
val customerWalletAddress = requestHelper.getCustomerWalletAddress()
val orderStatus = getOrderStatus().getOrNull() ?: error("Order status is null")
when (val orderId = tangemPayStorage.getOrderId(customerWalletAddress)) {
// If order id wasn't saved -> get customer info
null -> {
MainScreenCustomerInfo(
info = getCustomerInfoWithPersistedToken(),
orderStatus = OrderStatus.UNKNOWN,
)
}
// If order id was saved -> check its status
else -> {
val orderStatus = getOrderStatus(orderId)
val customerInfo = when (orderStatus) {
// Kyc is passed and user waits for order creation -> no need to get customer info
OrderStatus.NEW,
OrderStatus.PROCESSING,
-> CustomerInfo(productInstance = null, isKycApproved = true, cardInfo = null)
MainScreenCustomerInfo(info = getCustomerInfo(result), orderStatus = orderStatus)
// Order was created/cancelled -> clear order id and get customer info
OrderStatus.UNKNOWN,
OrderStatus.COMPLETED,
OrderStatus.CANCELED,
-> getCustomerInfoWithPersistedToken().also {
tangemPayStorage.clearOrderId(customerWalletAddress)
}
}
MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus)
}
}
}
}
@ -84,27 +108,28 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
return CustomerInfo(
productInstance = response?.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
kycStatus = response?.kyc?.status,
isKycApproved = response?.kyc?.status == APPROVED_KYC_STATUS,
cardInfo = cardInfo,
)
}
private suspend fun getOrderStatus(): Either<UniversalError, OrderStatus> {
return requestHelper.runWithErrorLogs(TAG) {
val walletAddress = requestHelper.getCustomerWalletAddress()
val orderId: String = tangemPayStorage.getOrderId(walletAddress)
?: return@runWithErrorLogs OrderStatus.NOT_ISSUED
private suspend fun getOrderStatus(orderId: String): OrderStatus {
val result = requestHelper.request { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}.result ?: error("Order result is null")
val result = requestHelper.request { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}.result ?: error("Order result is null")
when (result.status) {
OrderStatus.NEW.apiName -> OrderStatus.NEW
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
else -> OrderStatus.CANCELED
}
return when (result.status) {
OrderStatus.NEW.apiName -> OrderStatus.NEW
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
else -> OrderStatus.CANCELED
}
}
private suspend fun getCustomerInfoWithPersistedToken(): CustomerInfo {
val result = requestHelper.requestWithPersistedToken { authHeader ->
tangemPayApi.getCustomerMe(authHeader)
}.result
return getCustomerInfo(result)
}
}

View file

@ -86,6 +86,6 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
}.result
val items = TangemPayTxHistoryItemConverter.convertList(result.transactions).filterNotNull()
txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items)
}
}.onLeft { error(it.toString()) }
}
}

View file

@ -30,7 +30,7 @@ internal object TangemPayTxHistoryItemConverter :
merchantName = spend.merchantName,
enrichedMerchantCategory = spend.enrichedMerchantCategory,
merchantCategory = spend.merchantCategory,
status = spend.status,
status = TangemPayTxHistoryItemStatusConverter.convert(spend.status),
enrichedMerchantIconUrl = spend.enrichedMerchantIcon,
)
}

View file

@ -0,0 +1,16 @@
package com.tangem.data.visa.utils
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.utils.converter.Converter
internal object TangemPayTxHistoryItemStatusConverter : Converter<String, TangemPayTxHistoryItem.Status> {
override fun convert(value: String): TangemPayTxHistoryItem.Status {
return when (value.uppercase()) {
"PENDING" -> TangemPayTxHistoryItem.Status.PENDING
"RESERVED" -> TangemPayTxHistoryItem.Status.RESERVED
"COMPLETED" -> TangemPayTxHistoryItem.Status.COMPLETED
"DECLINED" -> TangemPayTxHistoryItem.Status.DECLINED
else -> TangemPayTxHistoryItem.Status.UNKNOWN
}
}
}

View file

@ -1,64 +0,0 @@
package com.tangem.data.yield.supply
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.data.yield.supply.converters.YieldMarketTokenConverter
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
import com.tangem.data.yield.supply.converters.YieldTokenStatusConverter
import com.tangem.data.yield.supply.converters.YieldTokenChartConverter
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlin.collections.map
internal class DefaultYieldSupplyMarketRepository(
private val yieldSupplyApi: YieldSupplyApi,
private val store: YieldMarketsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : YieldSupplyMarketRepository {
override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) {
store.getSyncOrNull()?.enrichNetworkIds()
}
override suspend fun updateMarkets(): List<YieldMarketToken> = withContext(dispatchers.io) {
val response = yieldSupplyApi.getYieldMarkets().getOrThrow()
val domain = response.marketDtos.map(YieldMarketTokenConverter::convert)
store.store(domain)
domain
}
override fun getMarketsFlow(): Flow<List<YieldMarketToken>> = store.get().map {
it.enrichNetworkIds()
}
override suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketTokenStatus {
val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId()
?: error("Chain id is required for evm's")
val response = yieldSupplyApi.getYieldTokenStatus(chainId, cryptoCurrencyToken.contractAddress).getOrThrow()
return YieldTokenStatusConverter.convert(response)
}
override suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData {
val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId()
?: error("Chain id is required for evm's")
val response = yieldSupplyApi.getYieldTokenChart(chainId, cryptoCurrencyToken.contractAddress).getOrThrow()
return YieldTokenChartConverter.convert(response)
}
private fun List<YieldMarketToken>.enrichNetworkIds(): List<YieldMarketToken> {
val chainIdMap = Blockchain.entries.associate { it.getChainId() to it.toNetworkId() }
return this.map { token ->
token.copy(backendId = chainIdMap[token.chainId])
}
}
}

View file

@ -0,0 +1,106 @@
package com.tangem.data.yield.supply
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.yieldsupply.YieldSupplyProvider
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.data.yield.supply.converters.YieldMarketTokenConverter
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
import com.tangem.data.yield.supply.converters.YieldTokenChartConverter
import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlin.collections.map
internal class DefaultYieldSupplyRepository(
private val yieldSupplyApi: YieldSupplyApi,
private val store: YieldMarketsStore,
private val walletManagersFacade: WalletManagersFacade,
private val dispatchers: CoroutineDispatcherProvider,
) : YieldSupplyRepository {
override suspend fun getCachedMarkets(): List<YieldMarketToken>? = withContext(dispatchers.io) {
val cache = store.getSyncOrNull().orEmpty()
val domain = cache.map(YieldMarketTokenConverter::convert)
domain.enrichNetworkIds()
}
override suspend fun updateMarkets(): List<YieldMarketToken> = withContext(dispatchers.io) {
val chains = Blockchain.yieldSupplySupportedBlockchains().map { it.getChainId() }.joinToString(",")
val response = yieldSupplyApi.getYieldMarkets(chainId = chains).getOrThrow()
val domain = response.marketDtos.map(YieldMarketTokenConverter::convert)
store.store(response.marketDtos)
domain
}
override fun getMarketsFlow(): Flow<List<YieldMarketToken>> = store.get().map {
it.map(YieldMarketTokenConverter::convert).enrichNetworkIds()
}
override suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketToken {
val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId()
?: error("Chain id is required for evm's")
val response = yieldSupplyApi.getYieldTokenStatus(chainId, cryptoCurrencyToken.contractAddress).getOrThrow()
return YieldMarketTokenConverter.convert(response)
}
override suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData {
val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId()
?: error("Chain id is required for evm's")
val response = yieldSupplyApi.getYieldTokenChart(chainId, cryptoCurrencyToken.contractAddress).getOrThrow()
return YieldTokenChartConverter.convert(response)
}
override suspend fun isYieldSupplySupported(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean =
withContext(dispatchers.io) {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = cryptoCurrency.network.toBlockchain(),
derivationPath = cryptoCurrency.network.derivationPath.value,
) ?: error("Wallet manager not found")
(walletManager as? YieldSupplyProvider)?.isSupported() ?: false
}
override suspend fun activateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean =
withContext(dispatchers.io) {
val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId()
?: error("Chain id is required for evm's")
yieldSupplyApi.activateYieldModule(
YieldSupplyChangeTokenStatusBody(
tokenAddress = cryptoCurrencyToken.contractAddress,
chainId = chainId,
),
).getOrThrow().isActive
}
override suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean =
withContext(dispatchers.io) {
val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId()
?: error("Chain id is required for evm's")
yieldSupplyApi.deactivateYieldModule(
YieldSupplyChangeTokenStatusBody(
tokenAddress = cryptoCurrencyToken.contractAddress,
chainId = chainId,
),
).getOrThrow().isActive
}
private fun List<YieldMarketToken>.enrichNetworkIds(): List<YieldMarketToken> {
val chainIdMap = Blockchain.entries.associate { it.getChainId() to it.toNetworkId() }
return this.map { token ->
token.copy(backendId = chainIdMap[token.chainId])
}
}
}

View file

@ -1,16 +1,19 @@
package com.tangem.data.yield.supply.converters
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
internal object YieldMarketTokenConverter : Converter<YieldMarketsResponse.MarketDto, YieldMarketToken> {
override fun convert(value: YieldMarketsResponse.MarketDto): YieldMarketToken {
internal object YieldMarketTokenConverter : Converter<YieldSupplyMarketTokenDto, YieldMarketToken> {
override fun convert(value: YieldSupplyMarketTokenDto): YieldMarketToken {
return YieldMarketToken(
tokenAddress = value.tokenAddress.orEmpty(),
apy = value.apy,
isActive = value.isActive,
apy = value.apy.orZero(),
isActive = value.isActive ?: false,
chainId = value.chainId ?: -1,
maxFeeUSD = value.maxFeeUSD.orEmpty(),
maxFeeNative = value.maxFeeNative.orEmpty(),
)
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.data.yield.supply.converters
import com.tangem.datasource.api.tangemTech.models.YieldTokenStatusResponse
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus
import com.tangem.utils.converter.Converter
internal object YieldTokenStatusConverter : Converter<YieldTokenStatusResponse, YieldMarketTokenStatus> {
override fun convert(value: YieldTokenStatusResponse): YieldMarketTokenStatus {
return YieldMarketTokenStatus(
tokenAddress = value.tokenAddress.orEmpty(),
tokenSymbol = value.tokenSymbol.orEmpty(),
tokenName = value.tokenName.orEmpty(),
apy = value.apy ?: SerializedBigDecimal.ZERO,
isActive = value.isActive ?: false,
chainId = value.chainId ?: -1,
maxFeeUSD = value.maxFeeUSD.orEmpty(),
maxFeeNative = value.maxFeeNative.orEmpty(),
)
}
}

View file

@ -1,12 +1,12 @@
package com.tangem.data.yield.supply.di
import com.tangem.data.yield.supply.DefaultYieldSupplyMarketRepository
import com.tangem.data.yield.supply.DefaultYieldSupplyRepository
import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver
import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyMarketRepository
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -37,12 +37,14 @@ internal object YieldSupplyDataModule {
fun provideYieldSupplyMarketRepository(
yieldSupplyApi: YieldSupplyApi,
store: YieldMarketsStore,
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
): YieldSupplyMarketRepository {
return DefaultYieldSupplyMarketRepository(
): YieldSupplyRepository {
return DefaultYieldSupplyRepository(
yieldSupplyApi = yieldSupplyApi,
store = store,
dispatchers = dispatchers,
walletManagersFacade = walletManagersFacade,
)
}