Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-15 14:25:11 +03:00
commit 5ee5b4f61b
563 changed files with 13917 additions and 4692 deletions

View file

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

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

@ -50,7 +50,9 @@ internal class CryptoPortfolioConverter @AssistedInject constructor(
derivationIndex = value.derivationIndex.value,
icon = value.icon.value.name,
iconColor = value.icon.color.name,
tokens = value.cryptoCurrencies.map(userTokensResponseFactory::createResponseToken),
tokens = value.cryptoCurrencies.map {
userTokensResponseFactory.createResponseToken(currency = it, accountId = value.accountId)
},
)
}

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

@ -41,6 +41,7 @@ internal object AccountDataModule {
walletAccountsSaver: WalletAccountsSaver,
accountsResponseStoreFactory: AccountsResponseStoreFactory,
userWalletsStore: UserWalletsStore,
userTokensSaver: UserTokensSaver,
eTagsStore: ETagsStore,
accountConverterFactoryContainer: AccountConverterFactoryContainer,
dispatchers: CoroutineDispatcherProvider,
@ -51,6 +52,7 @@ internal object AccountDataModule {
accountsResponseStoreFactory = accountsResponseStoreFactory,
archivedAccountsStoreFactory = ArchivedAccountsStoreFactory,
userWalletsStore = userWalletsStore,
userTokensSaver = userTokensSaver,
eTagsStore = eTagsStore,
convertersContainer = accountConverterFactoryContainer,
dispatchers = dispatchers,

View file

@ -0,0 +1,26 @@
package com.tangem.data.account.di
import com.tangem.data.account.producer.AccountListCryptoCurrenciesProducer
import com.tangem.data.account.producer.DefaultMultiWalletCryptoCurrenciesProducer
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object MultiWalletCryptoCurrenciesProducerModule {
@Singleton
@Provides
fun provideMultiWalletCryptoCurrenciesProducerFactory(
accountsFeatureToggles: AccountsFeatureToggles,
defaultImpl: DefaultMultiWalletCryptoCurrenciesProducer.Factory,
accountsImpl: AccountListCryptoCurrenciesProducer.Factory,
): MultiWalletCryptoCurrenciesProducer.Factory {
return if (accountsFeatureToggles.isFeatureEnabled) accountsImpl else defaultImpl
}
}

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

@ -0,0 +1,64 @@
package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.some
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.*
/**
* Implementation of [MultiWalletCryptoCurrenciesProducer] that produces crypto currencies of all accounts
*
* @property params params
* @property userWalletsStore UserWallet's store
* @property accountsResponseStoreFactory factory to create store with accounts response
* @property responseCryptoCurrenciesFactory factory for creating [CryptoCurrency] from `UserTokensResponse`
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor(
@Assisted val params: MultiWalletCryptoCurrenciesProducer.Params,
private val userWalletsStore: UserWalletsStore,
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiWalletCryptoCurrenciesProducer {
override val fallback: Option<Set<CryptoCurrency>> = emptySet<CryptoCurrency>().some()
override fun produce(): Flow<Set<CryptoCurrency>> {
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
if (!userWallet.isMultiCurrency) {
error("${this::class.simpleName} supports only multi-currency wallet")
}
return accountsResponseStoreFactory.create(userWalletId = userWallet.walletId).data
.distinctUntilChanged()
.map { response ->
if (response == null) return@map emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
response = response.toUserTokensResponse(),
userWallet = userWallet,
).toSet()
}
.onEmpty { emit(emptySet()) }
.flowOn(dispatchers.default)
}
@AssistedFactory
interface Factory : MultiWalletCryptoCurrenciesProducer.Factory {
override fun create(params: MultiWalletCryptoCurrenciesProducer.Params): AccountListCryptoCurrenciesProducer
}
}

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

@ -1,4 +1,4 @@
package com.tangem.data.tokens
package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.some

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

@ -9,8 +9,10 @@ import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.store.ArchivedAccountsStore
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
@ -24,9 +26,11 @@ 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
import timber.log.Timber
/**
[REDACTED_AUTHOR]
@ -38,6 +42,7 @@ internal class DefaultAccountsCRUDRepository(
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory,
private val userWalletsStore: UserWalletsStore,
private val userTokensSaver: UserTokensSaver,
private val eTagsStore: ETagsStore,
private val convertersContainer: AccountConverterFactoryContainer,
private val dispatchers: CoroutineDispatcherProvider,
@ -101,12 +106,44 @@ 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 syncTokens(userWalletId: UserWalletId) {
val response = getAccountsResponseSync(userWalletId = userWalletId)
if (response == null) {
Timber.e("Can't sync tokens. No accounts response found for wallet: $userWalletId")
return
}
userTokensSaver.push(userWalletId = userWalletId, response = response.toUserTokensResponse())
}
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

@ -1,4 +1,4 @@
package com.tangem.data.tokens
package com.tangem.data.account.producer
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain

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

@ -11,6 +11,7 @@ import com.tangem.data.account.store.ArchivedAccountsStore
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
@ -25,7 +26,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 +37,7 @@ import kotlin.time.Duration.Companion.minutes
/**
[REDACTED_AUTHOR]
*/
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultAccountsCRUDRepositoryTest {
@ -52,6 +53,7 @@ class DefaultAccountsCRUDRepositoryTest {
private val archivedAccountsStore = ArchivedAccountsStore(runtimeStore = archivedAccountsInnerStore)
private val userWalletsStore: UserWalletsStore = mockk()
private val userTokensSaver: UserTokensSaver = mockk()
private val eTagsStore: ETagsStore = mockk()
private val convertersContainer: AccountConverterFactoryContainer = mockk()
@ -64,6 +66,7 @@ class DefaultAccountsCRUDRepositoryTest {
accountsResponseStoreFactory = accountsResponseStoreFactory,
archivedAccountsStoreFactory = archivedAccountsStoreFactory,
userWalletsStore = userWalletsStore,
userTokensSaver = userTokensSaver,
eTagsStore = eTagsStore,
convertersContainer = convertersContainer,
dispatchers = TestingCoroutineDispatcherProvider(),
@ -580,22 +583,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 +603,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 +636,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

@ -1,5 +1,6 @@
package com.tangem.data.common.currency
import com.tangem.blockchain.blockchains.ethereum.Chain
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
@ -73,6 +74,17 @@ class CryptoCurrencyFactory(
)
}
fun createCoin(chainId: Int, extraDerivationPath: String?, userWallet: UserWallet): CryptoCurrency.Coin? {
val blockchain: Blockchain? = Chain.entries.find { it.id == chainId }?.blockchain
return if (blockchain != null) {
createCoin(blockchain, extraDerivationPath, userWallet)
} else {
Timber.e("Unable to get blockchain from chainId == $chainId")
null
}
}
fun createCoin(
blockchain: Blockchain,
extraDerivationPath: String?,

View file

@ -81,7 +81,10 @@ private fun getCurrencyIdBody(network: Network): CurrencyIdBody {
rawId = network.rawId,
derivationPath = path.value,
)
is Network.DerivationPath.Card,
is Network.DerivationPath.Card -> CurrencyIdBody.NetworkIdWithDerivationPath(
rawId = network.rawId,
derivationPath = path.value,
)
is Network.DerivationPath.None,
-> CurrencyIdBody.NetworkId(network.rawId)
}

View file

@ -1,18 +1,21 @@
package com.tangem.data.common.currency
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency
import javax.inject.Inject
// TODO: [REDACTED_JIRA]
class UserTokensResponseFactory @Inject constructor() {
fun createUserTokensResponse(
currencies: List<CryptoCurrency>,
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
accountId: AccountId? = null,
): UserTokensResponse {
return UserTokensResponse(
tokens = currencies.map(::createResponseToken),
tokens = currencies.map { createResponseToken(currency = it, accountId = accountId) },
group = if (isGroupedByNetwork) {
UserTokensResponse.GroupType.NETWORK
} else {
@ -26,10 +29,11 @@ class UserTokensResponseFactory @Inject constructor() {
)
}
fun createResponseToken(currency: CryptoCurrency): UserTokensResponse.Token {
fun createResponseToken(currency: CryptoCurrency, accountId: AccountId? = null): UserTokensResponse.Token {
return with(currency) {
UserTokensResponse.Token(
id = id.rawCurrencyId?.value,
accountId = accountId?.value,
networkId = network.backendId,
derivationPath = network.derivationPath.value,
name = name,

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

@ -33,6 +33,7 @@ dependencies {
// region Project - Libs
implementation(projects.libs.blockchainSdk)
implementation(tangemDeps.blockchain) { exclude(module = "joda-time") }
// endregion
// region DI

View file

@ -0,0 +1,98 @@
package com.tangem.data.networks.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId.Companion.CONTRACT_ADDRESS_DELIMITER
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.utils.converter.TwoWayConverter
import com.tangem.domain.models.currency.CryptoCurrency.ID.Suffix as CurrencyIdSuffix
/**
* Converts between [CurrencyId] and [CryptoCurrency.ID].
*
* @property rawNetworkId the raw network ID associated with the currency
* @property derivationPath the derivation path used for the network
*
[REDACTED_AUTHOR]
*/
internal class CurrencyIdConverter(
private val rawNetworkId: String,
private val derivationPath: Network.DerivationPath,
) : TwoWayConverter<CurrencyId, CryptoCurrency.ID> {
override fun convert(value: CurrencyId): CryptoCurrency.ID {
val suffixParts = value.value.split(CONTRACT_ADDRESS_DELIMITER)
val rawId = suffixParts.getOrNull(0)
val contractAddress = suffixParts.getOrNull(1)
return if (contractAddress.isNullOrBlank()) {
getCoinId(
coinId = rawId.takeUnless { it.isNullOrBlank() }
?: error("Coin id is null for $rawNetworkId with $derivationPath"),
)
} else {
getTokenId(
rawTokenId = rawId?.ifBlank { null },
contractAddress = contractAddress,
)
}
}
override fun convertBack(value: CryptoCurrency.ID): CurrencyId {
return if (value.isCoin) {
CurrencyId.createCoinId(
coinId = Blockchain.fromId(value.rawNetworkId).toCoinId(),
)
} else {
CurrencyId.createTokenId(
rawTokenId = value.rawCurrencyId?.value,
contractAddress = requireNotNull(value.contractAddress) {
"Token contractAddress is null for token id: $this"
},
)
}
}
private fun getCoinId(coinId: String): CryptoCurrency.ID {
return CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = getCurrencyIdBody(),
suffix = CurrencyIdSuffix.RawID(rawId = coinId),
)
}
private fun getTokenId(rawTokenId: String?, contractAddress: String): CryptoCurrency.ID {
val suffix = if (rawTokenId == null) {
CurrencyIdSuffix.ContractAddress(contractAddress)
} else {
CurrencyIdSuffix.RawID(rawTokenId, contractAddress)
}
return CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = getCurrencyIdBody(),
suffix = suffix,
)
}
private fun getCurrencyIdBody(): CryptoCurrency.ID.Body {
return when (derivationPath) {
is Network.DerivationPath.Card -> {
CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(
rawId = rawNetworkId,
derivationPath = derivationPath.value,
)
}
is Network.DerivationPath.Custom -> {
CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(
rawId = rawNetworkId,
derivationPath = derivationPath.value,
)
}
is Network.DerivationPath.None -> CryptoCurrency.ID.Body.NetworkId(rawNetworkId)
}
}
}

View file

@ -1,35 +1,46 @@
package com.tangem.data.networks.converters
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyAmount
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.utils.converter.TwoWayConverter
import com.tangem.utils.extensions.mapNotNullValues
import java.math.BigDecimal
private typealias AmountsDataModel = Map<String, BigDecimal>
private typealias AmountsDataModel = List<CurrencyAmount>
private typealias AmountsDomainModel = Map<CryptoCurrency.ID, NetworkStatus.Amount>
/**
* Converter from [AmountsDataModel] to [AmountsDomainModel] and vice versa
*
* @param rawNetworkId the raw network ID associated with the currency
* @param derivationPath the derivation path used for the network
*
[REDACTED_AUTHOR]
*/
internal object NetworkAmountsConverter : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
internal class NetworkAmountsConverter(
rawNetworkId: String,
derivationPath: Network.DerivationPath,
) : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
private val currencyIdConverter = CurrencyIdConverter(rawNetworkId, derivationPath)
override fun convert(value: AmountsDataModel): AmountsDomainModel {
return value
.mapKeys { CryptoCurrency.ID.fromValue(value = it.key) }
.mapValues { (_, amount) -> NetworkStatus.Amount.Loaded(value = amount) }
return value.associate {
val currencyId = currencyIdConverter.convert(value = it.id)
val amount = NetworkStatus.Amount.Loaded(value = it.amount)
currencyId to amount
}
}
override fun convertBack(value: AmountsDomainModel): AmountsDataModel {
return value
.mapKeys { (id, _) -> id.value }
.mapNotNullValues { (_, amount) ->
when (amount) {
is NetworkStatus.Amount.Loaded -> amount.value
is NetworkStatus.Amount.NotFound -> null
}
}
return value.mapNotNull {
val amount = it.value as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null
CurrencyAmount(
id = currencyIdConverter.convertBack(value = it.key),
amount = amount.value,
)
}
}
}

View file

@ -15,14 +15,22 @@ internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, Netwo
return when (val status = value.value) {
is NetworkStatus.Verified -> {
val address = NetworkAddressConverter.convertBack(value = status.address)
val amountsConverter = NetworkAmountsConverter(
rawNetworkId = value.network.rawId,
derivationPath = value.network.derivationPath,
)
val yieldSupplyStatusConverter = NetworkYieldSupplyStatusConverter(
rawNetworkId = value.network.rawId,
derivationPath = value.network.derivationPath,
)
NetworkStatusDM.Verified(
networkId = NetworkStatusDM.ID(value = value.network.rawId),
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
selectedAddress = address.selectedAddress,
availableAddresses = address.addresses,
amounts = NetworkAmountsConverter.convertBack(value = status.amounts),
yieldSupplyStatuses = NetworkYieldSupplyStatusConverter.convertBack(status.yieldSupplyStatuses),
amounts = amountsConverter.convertBack(value = status.amounts),
yieldSupplyStatuses = yieldSupplyStatusConverter.convertBack(status.yieldSupplyStatuses),
)
}
is NetworkStatus.NoAccount -> {

View file

@ -2,45 +2,43 @@ package com.tangem.data.networks.converters
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.utils.converter.TwoWayConverter
import com.tangem.utils.extensions.mapNotNullValues
private typealias YieldSupplyStatusDataModel = Map<String, NetworkStatusDM.YieldSupplyStatus?>
private typealias YieldSupplyStatusDataModel = List<NetworkStatusDM.YieldSupplyStatus>
private typealias YieldSupplyStatusDomainModel = Map<CryptoCurrency.ID, YieldSupplyStatus?>
internal object NetworkYieldSupplyStatusConverter :
TwoWayConverter<YieldSupplyStatusDataModel, YieldSupplyStatusDomainModel> {
internal class NetworkYieldSupplyStatusConverter(
rawNetworkId: String,
derivationPath: Network.DerivationPath,
) : TwoWayConverter<YieldSupplyStatusDataModel, YieldSupplyStatusDomainModel> {
private val currencyIdConverter = CurrencyIdConverter(rawNetworkId, derivationPath)
override fun convert(value: YieldSupplyStatusDataModel): YieldSupplyStatusDomainModel {
return value
.mapKeys { CryptoCurrency.ID.fromValue(value = it.key) }
.mapValues { (_, yieldSupplyStatus) ->
if (yieldSupplyStatus != null) {
YieldSupplyStatus(
isActive = yieldSupplyStatus.isActive,
isInitialized = yieldSupplyStatus.isInitialized,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,
)
} else {
null
}
}
return value.associate {
val id = currencyIdConverter.convert(value = it.id)
val status = YieldSupplyStatus(
isActive = it.isActive,
isInitialized = it.isInitialized,
isAllowedToSpend = it.isAllowedToSpend,
)
id to status
}
}
override fun convertBack(value: YieldSupplyStatusDomainModel): YieldSupplyStatusDataModel {
return value
.mapKeys { (id, _) -> id.value }
.mapNotNullValues { (_, yieldSupplyStatus) ->
if (yieldSupplyStatus != null) {
NetworkStatusDM.YieldSupplyStatus(
isActive = yieldSupplyStatus.isActive,
isInitialized = yieldSupplyStatus.isInitialized,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,
)
} else {
null
}
}
return value.mapNotNull { (currencyId, yieldSupplyStatus) ->
if (yieldSupplyStatus == null) return@mapNotNull null
NetworkStatusDM.YieldSupplyStatus(
id = currencyIdConverter.convertBack(value = currencyId),
isActive = yieldSupplyStatus.isActive,
isInitialized = yieldSupplyStatus.isInitialized,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,
)
}
}
}

View file

@ -22,14 +22,26 @@ internal object SimpleNetworkStatusConverter : Converter<NetworkStatusDM, Simple
),
)
val derivationPath = NetworkDerivationPathConverter.convert(value = value.derivationPath)
val amountsConverter = NetworkAmountsConverter(
rawNetworkId = value.networkId.value,
derivationPath = derivationPath,
)
val yieldSupplyStatusConverter = NetworkYieldSupplyStatusConverter(
rawNetworkId = value.networkId.value,
derivationPath = derivationPath,
)
val status = when (value) {
is NetworkStatusDM.Verified -> {
NetworkStatus.Verified(
address = address,
amounts = NetworkAmountsConverter.convert(value = value.amounts),
amounts = amountsConverter.convert(value = value.amounts),
pendingTransactions = emptyMap(),
source = StatusSource.CACHE,
yieldSupplyStatuses = NetworkYieldSupplyStatusConverter.convert(value = value.yieldSupplyStatuses),
yieldSupplyStatuses = yieldSupplyStatusConverter.convert(value = value.yieldSupplyStatuses),
)
}
is NetworkStatusDM.NoAccount -> {

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
@ -38,6 +40,7 @@ internal object NetworkDataModule {
dispatchers: CoroutineDispatcherProvider,
): NetworksStatusesStore {
return DefaultNetworksStatusesStore(
context = context,
runtimeStore = RuntimeSharedStore(),
persistenceDataStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
@ -45,7 +48,7 @@ internal object NetworkDataModule {
types = mapWithStringKeyTypes(valueTypes = setTypes<NetworkStatusDM>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "networks_statuses") },
produceFile = { context.dataStoreFile(fileName = "networks_statuses_2") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
dispatchers = dispatchers,
@ -67,4 +70,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

@ -1,5 +1,6 @@
package com.tangem.data.networks.store
import android.content.Context
import androidx.datastore.core.DataStore
import com.tangem.data.networks.converters.NetworkStatusDataModelConverter
import com.tangem.data.networks.converters.SimpleNetworkStatusConverter
@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
internal typealias WalletIdWithSimpleStatus = Map<String, Set<SimpleNetworkStatus>>
internal typealias WalletIdWithStatusDM = Map<String, Set<NetworkStatusDM>>
@ -27,11 +29,13 @@ internal typealias WalletIdWithStatusDM = Map<String, Set<NetworkStatusDM>>
/**
* Default implementation of [NetworksStatusesStore]
*
* @param context context
* @property runtimeStore runtime store
* @property persistenceDataStore persistence store
* @param dispatchers dispatchers
*/
internal class DefaultNetworksStatusesStore(
context: Context,
private val runtimeStore: RuntimeSharedStore<WalletIdWithSimpleStatus>,
private val persistenceDataStore: DataStore<WalletIdWithStatusDM>,
dispatchers: CoroutineDispatcherProvider,
@ -41,6 +45,16 @@ internal class DefaultNetworksStatusesStore(
init {
scope.launch {
try {
val oldFile = File(context.filesDir, "datastore/networks_statuses")
if (oldFile.exists()) {
oldFile.delete()
}
} catch (e: Exception) {
Timber.e(e, "Error while deleting old networks statuses datastore file")
}
val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch
runtimeStore.store(
@ -96,6 +110,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,190 @@
package com.tangem.data.networks.converters
import com.google.common.truth.Truth
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CurrencyIdConverterTest {
private val rawNetworkId = "ETH"
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
private val derivationPathHashCode = "-1843072795"
private val converter = CurrencyIdConverter(rawNetworkId = rawNetworkId, derivationPath = derivationPath)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Convert {
@ParameterizedTest
@ProvideTestModels
fun convert(model: ConvertModel) {
// Act
val actual = runCatching { converter.convert(value = model.value) }
// Assert
actual
.onSuccess {
Truth.assertThat(it).isEqualTo(model.expected.getOrNull())
}
.onFailure {
val expected = model.expected.exceptionOrNull()!!
Truth.assertThat(it).isInstanceOf(expected::class.java)
Truth.assertThat(it).hasMessageThat().isEqualTo(expected.message)
}
}
private fun provideTestModels(): Collection<ConvertModel> = listOf(
// create coin id
ConvertModel(
value = CurrencyId.createCoinId("ethereum"),
expected = Result.success(
CryptoCurrency.ID.fromValue(value = "coin⟨ETH→$derivationPathHashCode⟩ethereum"),
),
),
ConvertModel(
value = CurrencyId.createCoinId(""),
expected = Result.failure(
IllegalStateException("Coin id is null for $rawNetworkId with $derivationPath"),
),
),
ConvertModel(
value = CurrencyId.createCoinId(" "),
expected = Result.failure(
IllegalStateException("Coin id is null for $rawNetworkId with $derivationPath"),
),
),
// create token id
ConvertModel(
value = CurrencyId.createTokenId(
rawTokenId = "usdt",
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
expected = Result.success(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
ConvertModel(
value = CurrencyId.createTokenId(
rawTokenId = null,
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
expected = Result.success(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
ConvertModel(
value = CurrencyId.createTokenId(
rawTokenId = "",
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
expected = Result.success(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
ConvertModel(
value = CurrencyId.createTokenId(
rawTokenId = " ",
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
expected = Result.success(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
ConvertModel(
value = CurrencyId.createTokenId(
rawTokenId = "usdt",
contractAddress = "",
),
expected = Result.success(
CryptoCurrency.ID.fromValue(value = "coin⟨ETH→$derivationPathHashCode⟩usdt"),
),
),
ConvertModel(
value = CurrencyId.createTokenId(
rawTokenId = "usdt",
contractAddress = " ",
),
expected = Result.success(
CryptoCurrency.ID.fromValue(value = "coin⟨ETH→$derivationPathHashCode⟩usdt"),
),
),
)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class ConvertBack {
@ParameterizedTest
@ProvideTestModels
fun convertBack(model: ConvertBackModel) {
// Act
val actual = runCatching { converter.convertBack(value = model.value) }
// Assert
actual
.onSuccess {
Truth.assertThat(it).isEqualTo(model.expected.getOrNull())
}
.onFailure {
val expected = model.expected.exceptionOrNull()!!
Truth.assertThat(it).isInstanceOf(expected::class.java)
Truth.assertThat(it).hasMessageThat().isEqualTo(expected.message)
}
}
private fun provideTestModels(): Collection<ConvertBackModel> = listOf(
ConvertBackModel(
value = CryptoCurrency.ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum"),
expected = Result.success(
CurrencyId.createCoinId("ethereum"),
),
),
ConvertBackModel(
value = CryptoCurrency.ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
expected = Result.success(
CurrencyId.createTokenId(
rawTokenId = "usdt",
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
ConvertBackModel(
value = CryptoCurrency.ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
expected = Result.success(
CurrencyId.createTokenId(
rawTokenId = null,
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
)
}
data class ConvertModel(val value: CurrencyId, val expected: Result<CryptoCurrency.ID>)
data class ConvertBackModel(val value: CryptoCurrency.ID, val expected: Result<CurrencyId>)
}

View file

@ -1,16 +1,17 @@
package com.tangem.data.networks.converters
import com.google.common.truth.Truth
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.network.NetworkAddress
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class NetworkAddressConverterTest {
@Nested
@ -18,7 +19,7 @@ internal class NetworkAddressConverterTest {
inner class Convert {
@ParameterizedTest
@MethodSource("provideTestModels")
@ProvideTestModels
fun convert(model: ConvertModel) {
// Act
val actual = runCatching { NetworkAddressConverter.convert(value = model.value) }
@ -190,7 +191,7 @@ internal class NetworkAddressConverterTest {
inner class ConvertBack {
@ParameterizedTest
@MethodSource("provideTestModels")
@ProvideTestModels
fun convertBack(model: ConvertBackModel) {
// Act
val actual = NetworkAddressConverter.convertBack(value = model.value)

View file

@ -1,77 +1,99 @@
package com.tangem.data.networks.converters
import com.google.common.truth.Truth
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId
import com.tangem.domain.models.currency.CryptoCurrency.ID
import com.tangem.domain.models.currency.CryptoCurrency.ID.Body
import com.tangem.domain.models.currency.CryptoCurrency.ID.Prefix
import com.tangem.domain.models.network.NetworkStatus.Amount
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkStatus.Amount.Loaded
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class NetworkAmountsConverterTest {
private val rawNetworkId = "ETH"
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
private val derivationPathHashCode = "-1843072795"
private val converter = NetworkAmountsConverter(rawNetworkId = rawNetworkId, derivationPath = derivationPath)
@Test
fun convert() {
// Arrange
val value = mapOf(
"coin⟨BCH⟩bitcoin-cash" to BigDecimal.ZERO,
"coin⟨ETH→12367123⟩ethereum" to BigDecimal.ONE,
val value = listOf(
NetworkStatusDM.CurrencyAmount(CurrencyId.createCoinId(coinId = "ethereum"), BigDecimal.ONE),
NetworkStatusDM.CurrencyAmount(
id = CurrencyId.createTokenId(
rawTokenId = "usdt",
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
amount = BigDecimal.ZERO,
),
NetworkStatusDM.CurrencyAmount(
id = CurrencyId.createTokenId(
rawTokenId = null,
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
amount = BigDecimal.TEN,
),
)
// Act
val actual = NetworkAmountsConverter.convert(value)
val actual = converter.convert(value)
// Assert
val expected = mapOf(
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "BCH"),
suffix = ID.Suffix.RawID(rawId = "bitcoin-cash"),
ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
) to Loaded(value = BigDecimal.ZERO),
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkIdWithDerivationPath(rawId = "ETH", derivationPathHashCode = 12367123),
suffix = ID.Suffix.RawID(rawId = "ethereum"),
) to Loaded(value = BigDecimal.ONE),
ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
) to Loaded(value = BigDecimal.TEN),
)
Truth.assertThat(actual).isEqualTo(expected)
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
}
@Test
fun convertBack() {
// Arrange
val value = mapOf(
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "BCH"),
suffix = ID.Suffix.RawID(rawId = "bitcoin-cash"),
ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
) to Loaded(value = BigDecimal.ZERO),
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkIdWithDerivationPath(rawId = "ETH", derivationPathHashCode = 12367123),
suffix = ID.Suffix.RawID(rawId = "ethereum"),
) to Loaded(value = BigDecimal.ONE),
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "BTC"),
suffix = ID.Suffix.RawID(rawId = "bitcoin"),
) to Amount.NotFound,
ID.fromValue(
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
) to Loaded(value = BigDecimal.TEN),
)
// Act
val actual = NetworkAmountsConverter.convertBack(value)
val actual = converter.convertBack(value)
// Assert
val expected = mapOf(
"coin⟨BCH⟩bitcoin-cash" to BigDecimal.ZERO,
"coin⟨ETH→12367123⟩ethereum" to BigDecimal.ONE,
val expected = listOf(
NetworkStatusDM.CurrencyAmount(CurrencyId.createCoinId(coinId = "ethereum"), BigDecimal.ONE),
NetworkStatusDM.CurrencyAmount(
id = CurrencyId.createTokenId(
rawTokenId = "usdt",
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
amount = BigDecimal.ZERO,
),
NetworkStatusDM.CurrencyAmount(
id = CurrencyId.createTokenId(
rawTokenId = null,
contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
amount = BigDecimal.TEN,
),
)
Truth.assertThat(actual).isEqualTo(expected)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
}
}

View file

@ -2,7 +2,9 @@ package com.tangem.data.networks.converters
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.local.network.entity.NetworkStatusDM.*
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency.ID
import com.tangem.domain.models.currency.CryptoCurrency.ID.Body
@ -14,7 +16,6 @@ import com.tangem.domain.models.network.NetworkStatus.Amount
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import java.math.BigDecimal
/**
@ -26,7 +27,7 @@ internal class NetworkStatusDataModelConverterTest {
private val network: Network = MockCryptoCurrencyFactory().ethereum.network
@ParameterizedTest
@MethodSource("provideTestModels")
@ProvideTestModels
fun convert(model: ConvertModel) {
// Act
val actual = NetworkStatusDataModelConverter.convert(value = model.value)
@ -48,11 +49,7 @@ internal class NetworkStatusDataModelConverterTest {
),
),
amounts = mapOf(
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "BCH"),
suffix = ID.Suffix.RawID(rawId = "bitcoin-cash"),
) to Amount.Loaded(value = BigDecimal.ZERO),
ID.fromValue(value = "coin⟨ETH→0⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "BTC"),
@ -61,11 +58,7 @@ internal class NetworkStatusDataModelConverterTest {
),
pendingTransactions = mapOf(), // doesn't matter
yieldSupplyStatuses = mapOf(
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "BCH"),
suffix = ID.Suffix.RawID(rawId = "bitcoin-cash"),
) to YieldSupplyStatus(
ID.fromValue(value = "token⟨ETH→0⟩usdt⚓0x1") to YieldSupplyStatus(
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
@ -79,22 +72,25 @@ internal class NetworkStatusDataModelConverterTest {
source = StatusSource.ACTUAL, // doesn't matter
),
),
expected = NetworkStatusDM.Verified(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
expected = Verified(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "",
type = NetworkStatusDM.DerivationPath.Type.NONE,
type = DerivationPath.Type.NONE,
),
selectedAddress = "0x123",
availableAddresses = setOf(
NetworkStatusDM.Address(
Address(
value = "0x123",
type = NetworkStatusDM.Address.Type.Primary,
type = Address.Type.Primary,
),
),
amounts = mapOf("coin⟨BCH⟩bitcoin-cash" to BigDecimal.ZERO),
yieldSupplyStatuses = mapOf(
"coin⟨BCH⟩bitcoin-cash" to NetworkStatusDM.YieldSupplyStatus(
amounts = listOf(
CurrencyAmount(CurrencyId.createCoinId("ethereum"), BigDecimal.ZERO),
),
yieldSupplyStatuses = listOf(
YieldSupplyStatus(
id = CurrencyId.createTokenId("usdt", "0x1"),
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
@ -120,17 +116,17 @@ internal class NetworkStatusDataModelConverterTest {
source = StatusSource.ACTUAL, // doesn't matter
),
),
expected = NetworkStatusDM.NoAccount(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
expected = NoAccount(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "",
type = NetworkStatusDM.DerivationPath.Type.NONE,
type = DerivationPath.Type.NONE,
),
selectedAddress = "0x123",
availableAddresses = setOf(
NetworkStatusDM.Address(
Address(
value = "0x123",
type = NetworkStatusDM.Address.Type.Primary,
type = Address.Type.Primary,
),
),
amountToCreateAccount = BigDecimal.ONE,

View file

@ -2,82 +2,74 @@ package com.tangem.data.networks.converters
import com.google.common.truth.Truth
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId
import com.tangem.domain.models.currency.CryptoCurrency.ID
import com.tangem.domain.models.currency.CryptoCurrency.ID.Body
import com.tangem.domain.models.currency.CryptoCurrency.ID.Prefix
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class NetworkYieldSupplyStatusConverterTest {
private val rawNetworkId = "ETH"
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
private val derivationPathHashCode = "-1843072795"
private val converter = NetworkYieldSupplyStatusConverter(rawNetworkId, derivationPath)
private val domainStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
)
@Test
fun convert() {
// Arrange
val value = mapOf(
"coin⟨ETH⟩ethereum" to NetworkStatusDM.YieldSupplyStatus(
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
),
"coin⟨ETH→12367123⟩ethereum" to null,
val value = listOf(
createDataStatus(id = CurrencyId.createCoinId("ethereum")),
createDataStatus(id = CurrencyId.createTokenId("usdt", "0x1")),
)
// Act
val actual = NetworkYieldSupplyStatusConverter.convert(value)
val actual = converter.convert(value)
// Assert
val expected = mapOf(
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "ETH"),
suffix = ID.Suffix.RawID(rawId = "ethereum"),
) to YieldSupplyStatus(
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
),
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkIdWithDerivationPath(rawId = "ETH", derivationPathHashCode = 12367123),
suffix = ID.Suffix.RawID(rawId = "ethereum"),
) to null,
ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to domainStatus,
ID.fromValue("token⟨ETH→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
)
Truth.assertThat(actual).isEqualTo(expected)
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
}
@Test
fun convertBack() {
// Arrange
val value = mapOf(
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "ETH"),
suffix = ID.Suffix.RawID(rawId = "ethereum"),
) to YieldSupplyStatus(
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
),
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkIdWithDerivationPath(rawId = "ETH", derivationPathHashCode = 12367123),
suffix = ID.Suffix.RawID(rawId = "ethereum"),
) to null,
ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to domainStatus,
ID.fromValue("token⟨ETH→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
ID.fromValue("token⟨ETH→$derivationPathHashCode⟩usdc⚓0x1") to null,
)
// Act
val actual = NetworkYieldSupplyStatusConverter.convertBack(value)
val actual = converter.convertBack(value)
// Assert
val expected = mapOf(
"coin⟨ETH⟩ethereum" to NetworkStatusDM.YieldSupplyStatus(
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
),
val expected = listOf(
createDataStatus(id = CurrencyId.createCoinId("ethereum")),
createDataStatus(id = CurrencyId.createTokenId("usdt", "0x1")),
)
Truth.assertThat(actual).isEqualTo(expected)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
}
private fun createDataStatus(id: CurrencyId): NetworkStatusDM.YieldSupplyStatus {
return NetworkStatusDM.YieldSupplyStatus(
id = id,
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
)
}
}

View file

@ -2,12 +2,12 @@ package com.tangem.data.networks.converters
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.data.networks.models.SimpleNetworkStatus
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.local.network.entity.NetworkStatusDM.*
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency.ID
import com.tangem.domain.models.currency.CryptoCurrency.ID.Body
import com.tangem.domain.models.currency.CryptoCurrency.ID.Prefix
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.network.NetworkStatus
@ -15,7 +15,6 @@ import com.tangem.domain.models.network.NetworkStatus.Amount
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import java.math.BigDecimal
/**
@ -27,7 +26,7 @@ internal class SimpleNetworkStatusConverterTest {
private val network: Network = MockCryptoCurrencyFactory().ethereum.network
@ParameterizedTest
@MethodSource("provideTestModels")
@ProvideTestModels
fun convert(model: ConvertModel) {
// Act
val actual = runCatching { SimpleNetworkStatusConverter.convert(value = model.value) }
@ -48,34 +47,28 @@ internal class SimpleNetworkStatusConverterTest {
private fun provideTestModels() = listOf(
// region Verified
ConvertModel(
value = NetworkStatusDM.Verified(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
value = Verified(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "card",
type = NetworkStatusDM.DerivationPath.Type.CARD,
type = DerivationPath.Type.CARD,
),
selectedAddress = "0x1",
availableAddresses = setOf(
NetworkStatusDM.Address(
value = "0x1",
type = NetworkStatusDM.Address.Type.Primary,
),
NetworkStatusDM.Address(
value = "0x2",
type = NetworkStatusDM.Address.Type.Secondary,
),
Address(value = "0x1", type = Address.Type.Primary),
Address(value = "0x2", type = Address.Type.Secondary),
),
amounts = mapOf(
"coin⟨BCH⟩bitcoin-cash" to BigDecimal.ZERO,
"coin⟨ETH→12367123⟩ethereum" to BigDecimal.ONE,
amounts = listOf(
CurrencyAmount(CurrencyId.createCoinId("ethereum"), BigDecimal.ZERO),
CurrencyAmount(CurrencyId.createTokenId("usdt", "0x1"), BigDecimal.ZERO),
),
yieldSupplyStatuses = mapOf(
"coin⟨ETH⟩ethereum" to NetworkStatusDM.YieldSupplyStatus(
yieldSupplyStatuses = listOf(
YieldSupplyStatus(
id = CurrencyId.createCoinId("ethereum"),
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
),
"coin⟨ETH⟩ethereum" to null,
),
),
expected = SimpleNetworkStatus(
@ -101,33 +94,16 @@ internal class SimpleNetworkStatusConverterTest {
),
),
amounts = mapOf(
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "BCH"),
suffix = ID.Suffix.RawID(rawId = "bitcoin-cash"),
) to Amount.Loaded(value = BigDecimal.ZERO),
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkIdWithDerivationPath(rawId = "ETH", derivationPathHashCode = 12367123),
suffix = ID.Suffix.RawID(rawId = "ethereum"),
) to Amount.Loaded(value = BigDecimal.ONE),
ID.fromValue("coin⟨ETH→3046160⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
ID.fromValue("token⟨ETH→3046160⟩usdt⚓0x1") to Amount.Loaded(value = BigDecimal.ZERO),
),
pendingTransactions = emptyMap(),
yieldSupplyStatuses = mapOf(
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "ETH"),
suffix = ID.Suffix.RawID(rawId = "ethereum"),
) to YieldSupplyStatus(
ID.fromValue("coin⟨ETH→3046160⟩ethereum") to YieldSupplyStatus(
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
),
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "ETH"),
suffix = ID.Suffix.RawID(rawId = "ethereum"),
) to null,
),
source = StatusSource.CACHE,
),
@ -137,21 +113,21 @@ internal class SimpleNetworkStatusConverterTest {
// region NoAccount
ConvertModel(
value = NetworkStatusDM.NoAccount(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
value = NoAccount(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "card",
type = NetworkStatusDM.DerivationPath.Type.CARD,
type = DerivationPath.Type.CARD,
),
selectedAddress = "0x1",
availableAddresses = setOf(
NetworkStatusDM.Address(
Address(
value = "0x1",
type = NetworkStatusDM.Address.Type.Primary,
type = Address.Type.Primary,
),
NetworkStatusDM.Address(
Address(
value = "0x2",
type = NetworkStatusDM.Address.Type.Secondary,
type = Address.Type.Secondary,
),
),
amountToCreateAccount = BigDecimal.ONE,
@ -189,83 +165,83 @@ internal class SimpleNetworkStatusConverterTest {
// region Error
ConvertModel(
value = NetworkStatusDM.Verified(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
value = Verified(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "card",
type = NetworkStatusDM.DerivationPath.Type.CARD,
type = DerivationPath.Type.CARD,
),
selectedAddress = "0x1",
availableAddresses = setOf(
NetworkStatusDM.Address(
Address(
value = "0x2",
type = NetworkStatusDM.Address.Type.Primary,
type = Address.Type.Primary,
),
),
amounts = emptyMap(),
yieldSupplyStatuses = emptyMap(),
amounts = emptyList(),
yieldSupplyStatuses = emptyList(),
),
expected = Result.failure(
exception = IllegalArgumentException("Selected address must not be null"),
),
),
ConvertModel(
value = NetworkStatusDM.Verified(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
value = Verified(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "card",
type = NetworkStatusDM.DerivationPath.Type.CARD,
type = DerivationPath.Type.CARD,
),
selectedAddress = "0x1",
availableAddresses = setOf(),
amounts = emptyMap(),
yieldSupplyStatuses = emptyMap(),
amounts = emptyList(),
yieldSupplyStatuses = emptyList(),
),
expected = Result.failure(
exception = IllegalArgumentException("Selected address must not be null"),
),
),
ConvertModel(
value = NetworkStatusDM.Verified(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
value = Verified(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "card",
type = NetworkStatusDM.DerivationPath.Type.CARD,
type = DerivationPath.Type.CARD,
),
selectedAddress = "",
availableAddresses = setOf(
NetworkStatusDM.Address(
Address(
value = "0x1",
type = NetworkStatusDM.Address.Type.Primary,
type = Address.Type.Primary,
),
NetworkStatusDM.Address(
Address(
value = "0x2",
type = NetworkStatusDM.Address.Type.Secondary,
type = Address.Type.Secondary,
),
),
amounts = emptyMap(),
yieldSupplyStatuses = emptyMap(),
amounts = emptyList(),
yieldSupplyStatuses = emptyList(),
),
expected = Result.failure(
exception = IllegalArgumentException("Selected address must not be null"),
),
),
ConvertModel(
value = NetworkStatusDM.NoAccount(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
value = NoAccount(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "card",
type = NetworkStatusDM.DerivationPath.Type.CARD,
type = DerivationPath.Type.CARD,
),
selectedAddress = "",
availableAddresses = setOf(
NetworkStatusDM.Address(
Address(
value = "0x1",
type = NetworkStatusDM.Address.Type.Primary,
type = Address.Type.Primary,
),
NetworkStatusDM.Address(
Address(
value = "0x2",
type = NetworkStatusDM.Address.Type.Secondary,
type = Address.Type.Secondary,
),
),
amountToCreateAccount = BigDecimal.ONE,
@ -276,17 +252,17 @@ internal class SimpleNetworkStatusConverterTest {
),
),
ConvertModel(
value = NetworkStatusDM.NoAccount(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
value = NoAccount(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "card",
type = NetworkStatusDM.DerivationPath.Type.CARD,
type = DerivationPath.Type.CARD,
),
selectedAddress = "0x1",
availableAddresses = setOf(
NetworkStatusDM.Address(
Address(
value = "0x2",
type = NetworkStatusDM.Address.Type.Primary,
type = Address.Type.Primary,
),
),
amountToCreateAccount = BigDecimal.ONE,
@ -297,11 +273,11 @@ internal class SimpleNetworkStatusConverterTest {
),
),
ConvertModel(
value = NetworkStatusDM.NoAccount(
networkId = NetworkStatusDM.ID(network.rawId),
derivationPath = NetworkStatusDM.DerivationPath(
value = NoAccount(
networkId = ID(network.rawId),
derivationPath = DerivationPath(
value = "card",
type = NetworkStatusDM.DerivationPath.Type.CARD,
type = DerivationPath.Type.CARD,
),
selectedAddress = "0x1",
availableAddresses = setOf(),

View file

@ -11,6 +11,7 @@ import com.tangem.data.networks.toSimple
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -23,6 +24,7 @@ internal class GetTest {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -30,6 +30,7 @@ internal class InitializationTest {
every { persistenceStore.data } returns emptyFlow()
DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),
@ -44,6 +45,7 @@ internal class InitializationTest {
val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),
@ -66,6 +68,7 @@ internal class InitializationTest {
}
DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -27,6 +28,7 @@ internal class ParameterizedStoreStatusTest(private val model: Model) {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -27,6 +28,7 @@ internal class ParameterizedStoreSuccessTest(private val model: Model) {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -10,6 +10,7 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -26,6 +27,7 @@ internal class ParameterizedStoreTest(private val model: Model) {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,6 +25,7 @@ internal class SetSourceAsCacheTest {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,6 +25,7 @@ internal class SetSourceAsOnlyCacheTest {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -11,6 +11,7 @@ import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,6 +25,7 @@ internal class StoreStatusTest {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -10,6 +10,7 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -23,6 +24,7 @@ internal class StoreSuccessTest {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -10,6 +10,7 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -23,6 +24,7 @@ internal class StoreTest {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

View file

@ -14,6 +14,7 @@ import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -27,6 +28,7 @@ internal class UpdateStatusSourceTest {
private val persistenceStore = MockStateDataStore<WalletIdWithStatusDM>(default = emptyMap())
private val store = DefaultNetworksStatusesStore(
context = mockk(),
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
dispatchers = TestingCoroutineDispatcherProvider(),

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

@ -14,6 +14,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectList
import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -54,7 +55,7 @@ internal class DefaultSwapTransactionRepository(
storeTransactionState(
txId = transaction.txId,
status = it,
refundTokenCurrency = null,
accountWithCurrency = null,
)
}
appPreferencesStore.editData { mutablePreferences ->
@ -177,7 +178,7 @@ internal class DefaultSwapTransactionRepository(
override suspend fun storeTransactionState(
txId: String,
status: SwapStatusModel,
refundTokenCurrency: CryptoCurrency?,
accountWithCurrency: Pair<AccountId?, CryptoCurrency>?,
) {
appPreferencesStore.editData { mutablePreferences ->
val savedMap = mutablePreferences.getObjectMap<SwapStatusDTO>(
@ -187,8 +188,8 @@ internal class DefaultSwapTransactionRepository(
val updatesMap = savedMap.toMutableMap()
updatesMap[txId] = savedStatusConverter.convert(
status.copy(
refundTokensResponse = refundTokenCurrency?.let {
userTokensResponseFactory.createResponseToken(refundTokenCurrency)
refundTokensResponse = accountWithCurrency?.let { (accountId, currency) ->
userTokensResponseFactory.createResponseToken(currency, accountId)
},
),
)

View file

@ -25,10 +25,12 @@ internal class SavedSwapTransactionListConverter(
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
toCryptoCurrencyId = value.toCryptoCurrencyId,
fromTokensResponse = userTokensResponseFactory.createResponseToken(
value.fromCryptoCurrency,
currency = value.fromCryptoCurrency,
accountId = null,
),
toTokensResponse = userTokensResponseFactory.createResponseToken(
value.toCryptoCurrency,
currency = value.toCryptoCurrency,
accountId = null,
),
transactions = savedSwapTransactionConverter.convertList(value.transactions),
)
@ -74,8 +76,11 @@ internal class SavedSwapTransactionListConverter(
userWalletId = userWalletId.stringValue,
fromCryptoCurrencyId = fromCryptoCurrency.id.value,
toCryptoCurrencyId = toCryptoCurrency.id.value,
fromTokensResponse = userTokensResponseFactory.createResponseToken(fromCryptoCurrency),
toTokensResponse = userTokensResponseFactory.createResponseToken(toCryptoCurrency),
fromTokensResponse = userTokensResponseFactory.createResponseToken(
currency = fromCryptoCurrency,
accountId = null,
),
toTokensResponse = userTokensResponseFactory.createResponseToken(currency = toCryptoCurrency, accountId = null),
transactions = tokenTransactions,
)
}

View file

@ -1,20 +0,0 @@
package com.tangem.data.tokens.di
import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface MultiWalletCryptoCurrenciesProducerModule {
@Singleton
@Binds
fun bindMultiWalletCryptoCurrenciesProducerFactory(
impl: DefaultMultiWalletCryptoCurrenciesProducer.Factory,
): MultiWalletCryptoCurrenciesProducer.Factory
}

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

@ -0,0 +1,66 @@
package com.tangem.data.pay
import arrow.core.Either
import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.error.UniversalError
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.data.pay.util.TangemPayWalletsManager
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ReceiveAddressModel.NameService
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.pay.DataForReceive
import com.tangem.domain.pay.DataForReceiveFactory
import kotlinx.coroutines.CancellationException
import timber.log.Timber
import javax.inject.Inject
private const val TAG = "TangemPay: TokenReceiveConfigFactory"
internal class DefaultDataForReceiveFactory @Inject constructor(
@NetworkMoshi moshi: Moshi,
private val tangemPayWalletsManager: TangemPayWalletsManager,
excludedBlockchains: ExcludedBlockchains,
) : DataForReceiveFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains)
}
private val errorConverter by lazy(mode = LazyThreadSafetyMode.NONE) { TangemPayErrorConverter(moshi) }
override fun getDataForReceive(depositAddress: String, chainId: Int): Either<UniversalError, DataForReceive> {
return try {
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking()
/**
* Create [CryptoCurrency.Coin] only for F&F.
* Later will use [CryptoCurrency.Token] when contractAddresses will be provided by BFF.
*/
val currency = cryptoCurrencyFactory.createCoin(
chainId = chainId,
extraDerivationPath = null,
userWallet = wallet,
) ?: error("Cannot create crypto currency from chainId $chainId")
val result = DataForReceive(
currency = currency,
walletId = wallet.walletId,
receiveAddress = listOf(ReceiveAddressModel(nameService = NameService.Default, value = depositAddress)),
)
Either.Right(result)
} catch (exception: Exception) {
when (exception) {
is CancellationException -> {
throw exception
}
else -> {
Timber.tag(TAG).e(exception)
Either.Left(errorConverter.convert(exception))
}
}
}
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultDataForReceiveFactory
import com.tangem.data.pay.repository.DefaultCardDetailsRepository
import com.tangem.data.pay.repository.DefaultKycRepository
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
import com.tangem.data.pay.repository.DefaultOnboardingRepository
import com.tangem.domain.pay.DataForReceiveFactory
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.pay.repository.OnboardingRepository
@ -37,6 +39,10 @@ internal interface TangemPayDataModule {
@Singleton
fun bindCardDetailsRepository(repository: DefaultCardDetailsRepository): CardDetailsRepository
@Binds
@Singleton
fun bindDataForReceiveFactory(factory: DefaultDataForReceiveFactory): DataForReceiveFactory
companion object {
@Provides
@Singleton

View file

@ -74,6 +74,7 @@ internal class DefaultCardDetailsRepository @Inject constructor(
return when (env) {
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
ApiEnvironment.STAGE,
ApiEnvironment.MOCK,
-> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.dev

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)
}
}
}
}
@ -78,33 +102,35 @@ internal class DefaultOnboardingRepository @Inject constructor(
balance = balance.availableBalance,
currencyCode = balance.currency,
customerWalletAddress = paymentAccount.customerWalletAddress,
depositAddress = response.depositAddress,
)
} else {
null
}
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

@ -5,27 +5,19 @@ import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.error.UniversalError
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.data.pay.util.TangemPayWalletsManager
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@ -37,9 +29,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val tangemPayStorage: TangemPayStorage,
private val authDataSource: TangemPayAuthDataSource,
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val tangemPayWalletsManager: TangemPayWalletsManager,
private val walletManagersFacade: WalletManagersFacade,
private val networkFactory: NetworkFactory,
) {
@ -49,7 +39,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
private val refreshTokensMutex = Mutex()
private var refreshTokensJob: Deferred<VisaAuthTokens>? = null
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
private val errorConverter = TangemPayErrorConverter(moshi)
suspend fun <T : Any> runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either<UniversalError, T> {
return try {
@ -62,7 +52,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
}
else -> {
Timber.tag(tag).e(exception)
Either.Left(mapError(exception))
Either.Left(errorConverter.convert(exception))
}
}
}
@ -86,12 +76,6 @@ internal class TangemPayRequestPerformer @Inject constructor(
)
}
private fun getWallets(): Flow<List<UserWallet>> = if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWallets.map { requireNotNull(it) }
} else {
userWalletsListManager.userWallets
}
private suspend fun <T : Any> performRequest(
requestBlock: suspend (header: String) -> ApiResponse<T>,
getTokens: (suspend () -> VisaAuthTokens),
@ -143,11 +127,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
}
private suspend fun fetchAuthInputData(): AuthInputData {
val userWallets = getWallets()
.filter { it.isNotEmpty() }
.first()
val wallet = userWallets.find { it is UserWallet.Cold } as? UserWallet.Cold
?: error("Cannot find cold user wallet")
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay()
val network = networkFactory.create(
blockchain = Blockchain.Polygon,
@ -179,21 +159,6 @@ internal class TangemPayRequestPerformer @Inject constructor(
tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens)
return tokens
}
private fun mapError(throwable: Throwable): UniversalError {
return if (throwable is ApiResponseError.HttpException) {
val errorBody = throwable.errorBody ?: return VisaApiError.UnknownWithoutCode
return runCatching {
visaErrorAdapter.fromJson(errorBody)?.error?.code ?: throwable.code.numericCode
}.map {
VisaApiError.fromBackendError(it)
}.getOrElse {
VisaApiError.UnknownWithoutCode
}
} else {
VisaApiError.UnknownWithoutCode
}
}
}
internal data class AuthInputData(

View file

@ -0,0 +1,28 @@
package com.tangem.data.pay.util
import com.squareup.moshi.Moshi
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.converter.Converter
class TangemPayErrorConverter(moshi: Moshi) : Converter<Throwable, UniversalError> {
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
override fun convert(value: Throwable): UniversalError {
return if (value is ApiResponseError.HttpException) {
val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode
return runCatching {
visaErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode
}.map {
VisaApiError.fromBackendError(it)
}.getOrElse {
VisaApiError.UnknownWithoutCode
}
} else {
VisaApiError.UnknownWithoutCode
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.data.pay.util
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import javax.inject.Inject
internal class TangemPayWalletsManager @Inject constructor(
private val manager: UserWalletsListManager,
private val repository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) {
suspend fun getDefaultWalletForTangemPay(): UserWallet.Cold {
val userWalletsFlow = if (useNewRepository()) repository.userWallets else manager.userWallets
val userWallets = userWalletsFlow.filter { !it.isNullOrEmpty() }.first()
return findColdWallet(userWallets)
}
fun getDefaultWalletForTangemPayBlocking(): UserWallet.Cold {
val userWallets = if (useNewRepository()) repository.userWallets.value else manager.userWalletsSync
return findColdWallet(userWallets)
}
private fun useNewRepository(): Boolean = hotWalletFeatureToggles.isHotWalletEnabled
private fun findColdWallet(userWallets: List<UserWallet>?): UserWallet.Cold {
return userWallets?.find { it is UserWallet.Cold } as? UserWallet.Cold
?: error("Cannot find cold user wallet")
}
}

View file

@ -169,6 +169,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
return when (env) {
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
ApiEnvironment.STAGE,
ApiEnvironment.MOCK,
-> rsaPublicKey.dev

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

@ -54,7 +54,7 @@ internal class VisaApiRequestMaker @Inject constructor(
userWalletsStore.update(userWalletId) { userWallet ->
userWallet.requireColdWallet().copy(
scanResponse = userWallet.scanResponse.copy(
visaCardActivationStatus = VisaCardActivationStatus.RefreshTokenExpired,
// visaCardActivationStatus = VisaCardActivationStatus.RefreshTokenExpired,
),
)
}
@ -65,9 +65,9 @@ internal class VisaApiRequestMaker @Inject constructor(
userWalletsStore.update(userWalletId) { userWallet ->
userWallet.requireColdWallet().copy(
scanResponse = userWallet.scanResponse.copy(
visaCardActivationStatus = VisaCardActivationStatus.Activated(
visaAuthTokens = newTokens,
),
// visaCardActivationStatus = VisaCardActivationStatus.Activated(
// visaAuthTokens = newTokens,
// ),
),
)
}
@ -93,8 +93,9 @@ internal class VisaApiRequestMaker @Inject constructor(
@Throws
private fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens {
val userWallet = findVisaUserWallet(userWalletId)
val status = userWallet.requireColdWallet().scanResponse.visaCardActivationStatus
?: error("Visa card activation status not found")
// val status = userWallet.requireColdWallet().scanResponse.visaCardActivationStatus
// ?: error("Visa card activation status not found")
val status: VisaCardActivationStatus = TODO("Fix visaCardActivationStatus retrieval")
if (status is VisaCardActivationStatus.RefreshTokenExpired) {
throw RefreshTokenExpiredException()

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