Updated on 2026-08-14
This commit is contained in:
commit
a0c9ace219
1607 changed files with 40580 additions and 16776 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,13 +7,16 @@ import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher
|
|||
import com.tangem.data.account.repository.DefaultAccountsCRUDRepository
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
|
||||
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
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.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -38,6 +41,7 @@ internal object AccountDataModule {
|
|||
walletAccountsSaver: WalletAccountsSaver,
|
||||
accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
userTokensSaver: UserTokensSaver,
|
||||
eTagsStore: ETagsStore,
|
||||
accountConverterFactoryContainer: AccountConverterFactoryContainer,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -48,6 +52,7 @@ internal object AccountDataModule {
|
|||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
archivedAccountsStoreFactory = ArchivedAccountsStoreFactory,
|
||||
userWalletsStore = userWalletsStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
eTagsStore = eTagsStore,
|
||||
convertersContainer = accountConverterFactoryContainer,
|
||||
dispatchers = dispatchers,
|
||||
|
|
@ -61,4 +66,16 @@ internal object AccountDataModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletAccountsSaver(impl: DefaultWalletAccountsFetcher): WalletAccountsSaver = impl
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMainAccountTokensMigration(
|
||||
accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
userTokensSaver: UserTokensSaver,
|
||||
): MainAccountTokensMigration {
|
||||
return DefaultMainAccountTokensMigration(
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
userTokensSaver = userTokensSaver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import kotlinx.coroutines.coroutineScope
|
|||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Implementation of [MultiAccountListFetcher]
|
||||
|
|
@ -20,7 +21,7 @@ import java.util.concurrent.ConcurrentHashMap
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiAccountListFetcher(
|
||||
internal class DefaultMultiAccountListFetcher @Inject constructor(
|
||||
private val singleAccountListFetcher: SingleAccountListFetcher,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
) : MultiAccountListFetcher {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.account.fetcher
|
|||
import arrow.core.Either
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Implementation of [SingleAccountListFetcher]
|
||||
|
|
@ -11,7 +12,7 @@ import com.tangem.domain.account.fetcher.SingleAccountListFetcher
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultSingleAccountListFetcher(
|
||||
internal class DefaultSingleAccountListFetcher @Inject constructor(
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher,
|
||||
) : SingleAccountListFetcher {
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -11,9 +12,10 @@ 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.common.response.ApiResponseError.HttpException.Code
|
||||
import com.tangem.datasource.api.common.response.IF_NONE_MATCH_HEADER
|
||||
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)
|
||||
|
||||
|
|
@ -98,6 +106,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
if (error.isNetworkError(code = Code.PRECONDITION_FAILED)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -122,25 +132,41 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
|
||||
responseBody
|
||||
},
|
||||
onError = {
|
||||
onError = { throwable ->
|
||||
// pushWalletAccounts and storeWalletAccounts help to avoid cyclic dependency
|
||||
fetchWalletAccountsErrorHandler.handle(
|
||||
error = it,
|
||||
error = throwable,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = savedAccountsResponse,
|
||||
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)
|
||||
|
||||
pushAndStore(userWalletId = userWalletId, response = accountsResponseWithTokens)
|
||||
store(userWalletId = userWalletId, response = accountsResponseWithTokens)
|
||||
|
||||
userTokensSaver.push(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -153,7 +179,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun saveETag(userWalletId: UserWalletId, apiResponse: ApiResponse<*>) {
|
||||
val eTag = apiResponse.headers[IF_NONE_MATCH_HEADER]?.firstOrNull()
|
||||
val eTag = apiResponse.headers[ETAG_HEADER]?.firstOrNull()
|
||||
|
||||
if (eTag != null) {
|
||||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = eTag)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.data.tokens
|
||||
package com.tangem.data.account.producer
|
||||
|
||||
import arrow.core.Option
|
||||
import arrow.core.some
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -50,9 +55,9 @@ internal class WalletAccountListFlowFactory @Inject constructor(
|
|||
val currencies = if (isSingleWalletWithToken) {
|
||||
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet).toSet()
|
||||
} else {
|
||||
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet).let(::setOf)
|
||||
setOf(cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet))
|
||||
}
|
||||
|
||||
return AccountList.empty(userWallet = userWallet, cryptoCurrencies = currencies)
|
||||
return AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = currencies)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.SupervisorJob
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
typealias AccountsResponseStore = DataStore<GetWalletAccountsResponse?>
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ typealias AccountsResponseStore = DataStore<GetWalletAccountsResponse?>
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Singleton
|
||||
internal class AccountsResponseStoreFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
package com.tangem.data.account.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensureNotNull
|
||||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
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.utils.getSyncOrNull
|
||||
import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Implementation of [MainAccountTokensMigration] for migrating tokens associated with a main account.
|
||||
* The migration process involves transferring unassigned tokens from the main account to a selected account.
|
||||
*
|
||||
* @property accountsResponseStoreFactory Factory for creating stores to access cached account responses.
|
||||
* @property userTokensSaver Saver for updating user tokens in persistent storage.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMainAccountTokensMigration(
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
) : MainAccountTokensMigration {
|
||||
|
||||
override suspend fun migrate(
|
||||
userWalletId: UserWalletId,
|
||||
derivationIndex: DerivationIndex,
|
||||
): Either<Throwable, Unit> = either {
|
||||
if (derivationIndex == DerivationIndex.Main) {
|
||||
Timber.i("Migration skipped: derivation index is Main")
|
||||
return@either
|
||||
}
|
||||
|
||||
val store = accountsResponseStoreFactory.create(userWalletId)
|
||||
|
||||
val response = store.getSyncOrNull()
|
||||
|
||||
ensureNotNull(response) {
|
||||
val exception = IllegalStateException("No cached accounts response found")
|
||||
Timber.e(exception)
|
||||
exception
|
||||
}
|
||||
|
||||
val mainAccount = findAccount(response = response, derivationIndex = DerivationIndex.Main)
|
||||
val selectedAccount = findAccount(response = response, derivationIndex = derivationIndex)
|
||||
|
||||
val unassignedTokens = mainAccount.findUnassignedTokens(derivationIndex)
|
||||
|
||||
if (unassignedTokens == null) {
|
||||
Timber.i("No unassigned tokens found for migration")
|
||||
return@either
|
||||
}
|
||||
|
||||
val updatedResponse = response.copy(
|
||||
accounts = response.accounts.map { account ->
|
||||
when (account.id) {
|
||||
mainAccount.id -> {
|
||||
account.copy(tokens = account.tokens.orEmpty() - unassignedTokens)
|
||||
}
|
||||
selectedAccount.id -> {
|
||||
selectedAccount.assignTokens(userWalletId, unassignedTokens)
|
||||
}
|
||||
else -> account
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
userTokensSaver.push(
|
||||
userWalletId = userWalletId,
|
||||
response = updatedResponse.toUserTokensResponse(),
|
||||
onFailSend = {
|
||||
// TODO: save failed state to retry later
|
||||
// [REDACTED_JIRA]
|
||||
val exception = IllegalStateException("Failed to push updated tokens after migration")
|
||||
Timber.e(exception)
|
||||
raise(exception)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun Raise<Throwable>.findAccount(
|
||||
response: GetWalletAccountsResponse,
|
||||
derivationIndex: DerivationIndex,
|
||||
): WalletAccountDTO {
|
||||
val account = response.accounts.firstOrNull { it.derivationIndex == derivationIndex.value }
|
||||
|
||||
return ensureNotNull(account) {
|
||||
val exception = IllegalStateException("No account found with derivation index: $derivationIndex")
|
||||
Timber.e(exception)
|
||||
exception
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletAccountDTO.findUnassignedTokens(
|
||||
derivationIndex: DerivationIndex,
|
||||
): List<UserTokensResponse.Token>? {
|
||||
val tokens = this.tokens
|
||||
|
||||
if (tokens.isNullOrEmpty()) return tokens
|
||||
|
||||
return tokens
|
||||
.filterByDerivationIndex(derivationIndex)
|
||||
.map { it.copy(accountId = null) }
|
||||
.toNonEmptyListOrNull()
|
||||
}
|
||||
|
||||
private fun List<UserTokensResponse.Token>.filterByDerivationIndex(
|
||||
derivationIndex: DerivationIndex,
|
||||
): List<UserTokensResponse.Token> {
|
||||
return filter { savedToken ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId)
|
||||
if (blockchain == null) {
|
||||
Timber.e("Token has unknown networkId: $savedToken")
|
||||
return@filter false
|
||||
}
|
||||
|
||||
val derivationPathValue = savedToken.derivationPath
|
||||
if (derivationPathValue == null) {
|
||||
Timber.e("Token has no derivation path: $savedToken")
|
||||
return@filter false
|
||||
}
|
||||
|
||||
val accountNodeRecognizer = AccountNodeRecognizer(blockchain)
|
||||
val accountNodeValue = accountNodeRecognizer.recognize(derivationPathValue)
|
||||
|
||||
if (accountNodeValue == null) {
|
||||
Timber.e("Token has unrecognized derivation path: $savedToken")
|
||||
return@filter false
|
||||
}
|
||||
|
||||
accountNodeValue == derivationIndex.value.toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -54,4 +54,14 @@ internal fun List<WalletAccountDTO>.assignTokens(
|
|||
tokens = enrichedTokens[accountDTO.id].orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletAccountDTO.assignTokens(
|
||||
userWalletId: UserWalletId,
|
||||
tokens: List<UserTokensResponse.Token>,
|
||||
): WalletAccountDTO {
|
||||
val enrichedTokens = UserTokensResponseAccountIdEnricher(userWalletId, tokens)
|
||||
.filter { it.accountId == this.id }
|
||||
|
||||
return copy(tokens = enrichedTokens)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@ package com.tangem.data.account.fetcher
|
|||
|
||||
import com.google.common.truth.Truth
|
||||
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
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.IF_NONE_MATCH_HEADER
|
||||
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
||||
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
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
|
|
@ -24,6 +25,7 @@ import org.junit.jupiter.api.*
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("Unchecked_Cast", "UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultWalletAccountsFetcherTest {
|
||||
|
||||
|
|
@ -35,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(
|
||||
|
|
@ -42,6 +45,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
userTokensSaver = userTokensSaver,
|
||||
fetchWalletAccountsErrorHandler = fetchWalletAccountsErrorHandler,
|
||||
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
|
||||
eTagsStore = eTagsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
|
@ -78,14 +82,17 @@ class DefaultWalletAccountsFetcherTest {
|
|||
val savedAccountsResponse = null
|
||||
val unassignedToken = createToken(accountId = null)
|
||||
|
||||
val accounts = listOf(createWalletAccountDTO(userWalletId = userWalletId, tokens = null))
|
||||
val accountsResponse = createGetWalletAccountsResponse(
|
||||
userWalletId = userWalletId,
|
||||
unassignedTokens = listOf(unassignedToken),
|
||||
)
|
||||
.copy(accounts = accounts)
|
||||
|
||||
val newETag = "newEtag"
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = accountsResponse,
|
||||
headers = mapOf(IF_NONE_MATCH_HEADER to listOf(newETag)),
|
||||
headers = mapOf(ETAG_HEADER to listOf(newETag)),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = savedAccountsResponse
|
||||
|
|
@ -110,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)
|
||||
|
|
@ -124,12 +131,6 @@ class DefaultWalletAccountsFetcherTest {
|
|||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = newETag)
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts),
|
||||
)
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
}
|
||||
|
|
@ -156,7 +157,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
val newETag = "newEtag"
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = accountsResponse,
|
||||
headers = mapOf(IF_NONE_MATCH_HEADER to listOf(newETag)),
|
||||
headers = mapOf(ETAG_HEADER to listOf(newETag)),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = savedAccountsResponse
|
||||
|
|
@ -199,7 +200,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
fun `fetch should call error handler when getWalletAccounts returns error`() = runTest {
|
||||
// Arrange
|
||||
val savedAccountsResponse = null
|
||||
val apiError = ApiResponse.Error(ApiResponseError.NetworkException)
|
||||
val apiError = ApiResponse.Error(ApiResponseError.NetworkException())
|
||||
|
||||
accountsResponseStoreFlow.value = savedAccountsResponse
|
||||
|
||||
|
|
@ -207,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)
|
||||
|
||||
|
|
@ -232,87 +243,6 @@ class DefaultWalletAccountsFetcherTest {
|
|||
userTokensSaver.push(userWalletId = any(), response = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `push should throw error when saveWalletAccounts returns PRECONDITION_FAILED`() = runTest {
|
||||
// Arrange
|
||||
val savedAccountsResponse = null
|
||||
val unassignedToken = createToken(accountId = null)
|
||||
|
||||
val accountsResponse = createGetWalletAccountsResponse(
|
||||
userWalletId = userWalletId,
|
||||
unassignedTokens = listOf(unassignedToken),
|
||||
)
|
||||
|
||||
val newETag = "newEtag"
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = accountsResponse,
|
||||
headers = mapOf(IF_NONE_MATCH_HEADER to listOf(newETag)),
|
||||
)
|
||||
|
||||
val apiError = ApiResponseError.HttpException(
|
||||
code = ApiResponseError.HttpException.Code.PRECONDITION_FAILED,
|
||||
message = null,
|
||||
errorBody = null,
|
||||
)
|
||||
val saveApiResponse = ApiResponse.Error(apiError)
|
||||
|
||||
accountsResponseStoreFlow.value = savedAccountsResponse
|
||||
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val updatedAccountsResponse = accountsResponse.copy(
|
||||
accounts = accountsResponse.accounts.map {
|
||||
it.copy(tokens = listOf(unassignedToken.copy(accountId = accountId)))
|
||||
},
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
} returns apiResponse
|
||||
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns accountsResponse
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts),
|
||||
)
|
||||
} returns saveApiResponse as ApiResponse<Unit>
|
||||
|
||||
// Act
|
||||
val actual = runCatching { fetcher.fetch(userWalletId) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(apiError)
|
||||
|
||||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.data
|
||||
eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag)
|
||||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = newETag)
|
||||
accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts),
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
fetchWalletAccountsErrorHandler.handle(
|
||||
error = any(),
|
||||
userWalletId = any(),
|
||||
savedAccountsResponse = any(),
|
||||
pushWalletAccounts = any(),
|
||||
storeWalletAccounts = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
|
@ -343,28 +273,26 @@ class DefaultWalletAccountsFetcherTest {
|
|||
@Test
|
||||
fun `push should call saveWalletAccounts with correct params`() = runTest {
|
||||
// Arrange
|
||||
val accounts = listOf(
|
||||
mockk<WalletAccountDTO>(),
|
||||
)
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -373,7 +301,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
fun `push should throw error when saveWalletAccounts returns PRECONDITION_FAILED`() = runTest {
|
||||
// Arrange
|
||||
val accounts = listOf(
|
||||
mockk<WalletAccountDTO>(),
|
||||
createWalletAccountDTO(userWalletId = userWalletId, tokens = null),
|
||||
)
|
||||
val response = SaveWalletAccountsResponse(accounts)
|
||||
val apiError = ApiResponseError.HttpException(
|
||||
|
|
@ -388,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()!!
|
||||
|
|
@ -398,44 +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
|
||||
listOf(
|
||||
mockk<WalletAccountDTO>(),
|
||||
)
|
||||
val response = createGetWalletAccountsResponse(userWalletId)
|
||||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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,28 +104,21 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uses default accounts when savedAccountsResponse is null`() = runTest {
|
||||
// Arrange
|
||||
val error = ApiResponseError.TimeoutException
|
||||
|
||||
val accounts = AccountList.empty(userWallet).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
val error = ApiResponseError.TimeoutException()
|
||||
|
||||
val accountDTO = WalletAccountDTO(
|
||||
id = "nibh",
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -261,7 +261,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
val actual1 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// Assert
|
||||
val expected1 = producer.fallback
|
||||
val expected1 = producer.fallback.getOrNull()
|
||||
Truth.assertThat(actual1.size).isEqualTo(1)
|
||||
Truth.assertThat(actual1.first()).isEqualTo(expected1)
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
|||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = producer.fallback
|
||||
val expected = producer.fallback.getOrNull()
|
||||
Truth.assertThat(actual.size).isEqualTo(1)
|
||||
Truth.assertThat(actual.first()).isEqualTo(expected)
|
||||
|
||||
|
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -48,8 +51,14 @@ class WalletAccountListFlowFactoryTest {
|
|||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(accountListConverter)
|
||||
|
||||
clearMocks(
|
||||
userWalletsStore,
|
||||
accountsResponseStoreFactory,
|
||||
accountsResponseStore,
|
||||
accountListConverterFactory,
|
||||
accountListConverter,
|
||||
cardCryptoCurrencyFactory,
|
||||
)
|
||||
accountsResponseStoreFlow.value = null
|
||||
}
|
||||
|
||||
|
|
@ -61,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
|
||||
|
|
@ -94,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) {
|
||||
|
|
@ -121,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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,226 @@
|
|||
package com.tangem.data.account.token
|
||||
|
||||
import com.tangem.common.test.utils.assertEitherLeft
|
||||
import com.tangem.common.test.utils.assertEitherRight
|
||||
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.tokens.DefaultMainAccountTokensMigration
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
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
|
||||
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultMainAccountTokensMigrationTest {
|
||||
|
||||
private val accountsResponseStoreFactory = mockk<AccountsResponseStoreFactory>()
|
||||
private val accountsResponseStore = mockk<AccountsResponseStore>()
|
||||
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
|
||||
|
||||
private val userTokensSaver = mockk<UserTokensSaver>(relaxed = true)
|
||||
private val migration = DefaultMainAccountTokensMigration(
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
userTokensSaver = userTokensSaver,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val derivationIndex = DerivationIndex(1).getOrNull()!!
|
||||
|
||||
@BeforeEach
|
||||
fun setupAll() {
|
||||
every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore
|
||||
every { accountsResponseStore.data } returns accountsResponseStoreFlow
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(accountsResponseStoreFactory, accountsResponseStore, userTokensSaver)
|
||||
accountsResponseStoreFlow.value = null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate skips when derivation index is Main`() = runTest {
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, DerivationIndex.Main)
|
||||
|
||||
// Assert
|
||||
assertEitherRight(actual)
|
||||
|
||||
coVerify(inverse = true) {
|
||||
accountsResponseStoreFactory.create(any())
|
||||
accountsResponseStore.data
|
||||
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate fails when no cached accounts response`() = runTest {
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, derivationIndex)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException("No cached accounts response found")
|
||||
assertEitherLeft(actual, expected)
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate fails when selected account DTO not found`() = runTest {
|
||||
// Arrange
|
||||
val response = createGetWalletAccountsResponse(
|
||||
userWalletId = userWalletId,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = DerivationIndex.Main.value),
|
||||
),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, derivationIndex)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException("No account found with derivation index: $derivationIndex")
|
||||
assertEitherLeft(actual, expected)
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate skips when no unassigned tokens`() = runTest {
|
||||
// Arrange
|
||||
val response = createGetWalletAccountsResponse(
|
||||
userWalletId = userWalletId,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = DerivationIndex.Main.value),
|
||||
),
|
||||
)
|
||||
|
||||
val selectedAccount = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex).value,
|
||||
derivationIndex = derivationIndex.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = response.copy(accounts = response.accounts + selectedAccount)
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, derivationIndex)
|
||||
|
||||
// Assert
|
||||
assertEitherRight(actual)
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migrate updates tokens for selected account`() = runTest {
|
||||
// Arrange
|
||||
val unassignedToken = createBitcoin(accountIndex = 1)
|
||||
|
||||
val mainAccount = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main).value,
|
||||
derivationIndex = DerivationIndex.Main.value,
|
||||
tokens = listOf(
|
||||
createBitcoin(accountIndex = 0),
|
||||
unassignedToken,
|
||||
),
|
||||
)
|
||||
|
||||
val selectedAccount = createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex).value,
|
||||
derivationIndex = derivationIndex.value,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
),
|
||||
accounts = listOf(mainAccount, selectedAccount),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, derivationIndex)
|
||||
|
||||
// Assert
|
||||
assertEitherRight(actual)
|
||||
|
||||
val migratedResponse = response.copy(
|
||||
accounts = listOf(
|
||||
mainAccount.copy(tokens = mainAccount.tokens!! - unassignedToken),
|
||||
selectedAccount.copy(tokens = listOf(unassignedToken)),
|
||||
),
|
||||
)
|
||||
|
||||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
userTokensSaver.push(
|
||||
userWalletId = userWalletId,
|
||||
response = migratedResponse.toUserTokensResponse(),
|
||||
onFailSend = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBitcoin(accountIndex: Int): UserTokensResponse.Token {
|
||||
return UserTokensResponse.Token(
|
||||
id = "ne",
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex(accountIndex).getOrNull()!!,
|
||||
).value,
|
||||
networkId = "bitcoin",
|
||||
derivationPath = "m/44'/60'/$accountIndex'/0/0",
|
||||
name = "Phil Hinton",
|
||||
symbol = "graeci",
|
||||
decimals = 6487,
|
||||
contractAddress = "vim",
|
||||
addresses = listOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.domain.card.common
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
|
||||
class TwinsHelperTest {
|
||||
|
||||
private val pack1Twins = listOf(
|
||||
"CB610000021",
|
||||
"CB620000031",
|
||||
)
|
||||
|
||||
private val pack2Twins = listOf(
|
||||
"CB640000012",
|
||||
"CB650000011",
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `twins compatibility pack 1 success`() {
|
||||
Assert.assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack1Twins[1]))
|
||||
assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[1], pack1Twins[0]))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `twins compatibility pack 1 same cards`() {
|
||||
assertFalse(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack1Twins[0]))
|
||||
assertFalse(TwinsHelper.isTwinsCompatible(pack1Twins[1], pack1Twins[1]))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `twins compatibility pack 2 success`() {
|
||||
assertTrue(TwinsHelper.isTwinsCompatible(pack2Twins[0], pack2Twins[1]))
|
||||
assertTrue(TwinsHelper.isTwinsCompatible(pack2Twins[1], pack2Twins[0]))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `twins compatibility pack 2 same cards`() {
|
||||
assertFalse(TwinsHelper.isTwinsCompatible(pack2Twins[0], pack2Twins[0]))
|
||||
assertFalse(TwinsHelper.isTwinsCompatible(pack2Twins[1], pack2Twins[1]))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `twins compatibility false for different packs`() {
|
||||
assertFalse(TwinsHelper.isTwinsCompatible(pack2Twins[0], pack1Twins[1]))
|
||||
assertFalse(TwinsHelper.isTwinsCompatible(pack1Twins[1], pack2Twins[0]))
|
||||
assertFalse(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack2Twins[1]))
|
||||
assertFalse(TwinsHelper.isTwinsCompatible(pack2Twins[1], pack1Twins[0]))
|
||||
}
|
||||
}
|
||||
|
|
@ -37,14 +37,14 @@ internal class DefaultAnalyticsRepository(
|
|||
}
|
||||
|
||||
override suspend fun setWalletBalanceState(userWalletId: UserWalletId, balanceState: WalletBalanceState) {
|
||||
appPreferencesStore.editData {
|
||||
val walletsBalanceState = it.getObjectMap<WalletBalanceState>(
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val walletsBalanceState = preferences.getObjectMap<WalletBalanceState>(
|
||||
key = PreferencesKeys.WALLETS_BALANCES_STATES_KEY,
|
||||
)
|
||||
val updatedWalletsBalanceState = walletsBalanceState
|
||||
.plus(pair = userWalletId.stringValue to balanceState)
|
||||
|
||||
it.setObjectMap(PreferencesKeys.WALLETS_BALANCES_STATES_KEY, updatedWalletsBalanceState)
|
||||
preferences.setObjectMap(PreferencesKeys.WALLETS_BALANCES_STATES_KEY, updatedWalletsBalanceState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,8 @@ class DefaultDeviceFlipDetector @Inject constructor(
|
|||
) : DeviceFlipDetector, DefaultLifecycleObserver {
|
||||
|
||||
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
private var gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
|
||||
private var isResumedState = AtomicBoolean(false)
|
||||
private val gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
|
||||
private val isResumedState = AtomicBoolean(false)
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
isResumedState.set(false)
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ internal class FlipListener(private val action: () -> Unit) : SensorEventListene
|
|||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent?) {
|
||||
event?.let {
|
||||
if (event != null) {
|
||||
val currentTime = SystemClock.elapsedRealtime()
|
||||
val zAxisValue = it.values[2]
|
||||
val zAxisValue = event.values[2]
|
||||
|
||||
if (zAxisValue < zAxisThreshold && !isScreenDown) {
|
||||
isScreenDown = true
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ android {
|
|||
dependencies {
|
||||
/* Project - Domain */
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.blockaid)
|
||||
implementation(projects.domain.blockaid.models)
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ dependencies {
|
|||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.core)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.data.blockaid
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.BlockAidScanOptions
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.Data
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionBulkScanRequest
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.common.TransactionData as SDKTransactionData
|
||||
|
||||
internal class BlockAidEvmScanTransactionConverter(
|
||||
private val blockchain: Blockchain,
|
||||
) : Converter<List<SDKTransactionData.Uncompiled>, EvmTransactionBulkScanRequest> {
|
||||
|
||||
@Suppress("NullableToStringCall")
|
||||
override fun convert(value: List<SDKTransactionData.Uncompiled>): EvmTransactionBulkScanRequest {
|
||||
return EvmTransactionBulkScanRequest(
|
||||
chain = blockchain.getChainId().toString(),
|
||||
options = listOf(BlockAidScanOptions.GasEstimation.value),
|
||||
metadata = TransactionMetadata(domain = "https://tangem.com"),
|
||||
data = value.map { transactionData ->
|
||||
Data(
|
||||
from = transactionData.sourceAddress,
|
||||
to = transactionData.destinationAddress,
|
||||
data = (transactionData.extras as? EthereumTransactionExtras)?.callData?.dataHex.orEmpty(),
|
||||
)
|
||||
},
|
||||
aggregated = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -169,7 +169,7 @@ internal object BlockAidMapper {
|
|||
val tokenInfo = TokenInfo(
|
||||
chainId = exposure.asset.chainId,
|
||||
logoUrl = exposure.asset.logoUrl,
|
||||
symbol = exposure.asset.symbol ?: "",
|
||||
symbol = exposure.asset.symbol.orEmpty(),
|
||||
decimals = exposure.asset.decimals ?: 0,
|
||||
)
|
||||
return exposure.spenders.flatMap { (_, spender) ->
|
||||
|
|
@ -188,8 +188,8 @@ internal object BlockAidMapper {
|
|||
private fun mapApproveNftTransaction(exposure: Exposure): ApproveInfo.NonFungibleToken {
|
||||
return ApproveInfo.NonFungibleToken(
|
||||
name = exposure.asset.name.orEmpty(),
|
||||
logoUrl = exposure.spenders.values.firstOrNull()?.exposure?.firstOrNull()?.logoUrl
|
||||
?: exposure.asset.logoUrl,
|
||||
logoUrl = exposure.spenders.values.firstOrNull()
|
||||
?.exposure?.firstOrNull()?.logoUrl ?: exposure.asset.logoUrl,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +201,7 @@ internal object BlockAidMapper {
|
|||
val token = TokenInfo(
|
||||
chainId = diff.asset.chainId,
|
||||
logoUrl = diff.asset.logoUrl,
|
||||
symbol = diff.asset.symbol ?: "",
|
||||
symbol = diff.asset.symbol.orEmpty(),
|
||||
decimals = diff.asset.decimals ?: 0,
|
||||
)
|
||||
diff.outTransfer.orEmpty().forEach { transfer ->
|
||||
|
|
|
|||
|
|
@ -3,11 +3,19 @@ package com.tangem.data.blockaid
|
|||
import com.domain.blockaid.models.dapp.CheckDAppResult
|
||||
import com.domain.blockaid.models.dapp.DAppData
|
||||
import com.domain.blockaid.models.transaction.CheckTransactionResult
|
||||
import com.domain.blockaid.models.transaction.GasEstimationResult
|
||||
import com.domain.blockaid.models.transaction.TransactionData
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.blockchain.common.TransactionData as SDKTransactionData
|
||||
|
||||
interface BlockAidRepository {
|
||||
|
||||
suspend fun verifyDAppDomain(data: DAppData): CheckDAppResult
|
||||
|
||||
suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult
|
||||
|
||||
suspend fun getGasEstimation(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
transactionDataList: List<SDKTransactionData.Uncompiled>,
|
||||
): GasEstimationResult
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.blockaid
|
||||
|
||||
import arrow.core.Either
|
||||
import com.domain.blockaid.models.transaction.GasEstimationResult
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.domain.blockaid.BlockAidGasEstimate
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultBlockAidGasEstimate @Inject constructor(
|
||||
private val repository: BlockAidRepository,
|
||||
) : BlockAidGasEstimate {
|
||||
override suspend fun getGasEstimation(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
transactionDataList: List<TransactionData.Uncompiled>,
|
||||
): Either<Throwable, GasEstimationResult> = Either.catch {
|
||||
repository.getGasEstimation(cryptoCurrency = cryptoCurrency, transactionDataList = transactionDataList)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,18 @@ package com.tangem.data.blockaid
|
|||
import com.domain.blockaid.models.dapp.CheckDAppResult
|
||||
import com.domain.blockaid.models.dapp.DAppData
|
||||
import com.domain.blockaid.models.transaction.CheckTransactionResult
|
||||
import com.domain.blockaid.models.transaction.GasEstimationResult
|
||||
import com.domain.blockaid.models.transaction.TransactionData
|
||||
import com.domain.blockaid.models.transaction.TransactionParams
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.data.blockaid.converters.GasEstimationResponseConverter
|
||||
import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
||||
import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.tangem.blockchain.common.TransactionData as SDKTransactionData
|
||||
|
||||
internal class DefaultBlockAidRepository(
|
||||
private val api: BlockAidApi,
|
||||
|
|
@ -30,6 +36,17 @@ internal class DefaultBlockAidRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getGasEstimation(
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
transactionDataList: List<SDKTransactionData.Uncompiled>,
|
||||
): GasEstimationResult {
|
||||
val blockchain = cryptoCurrency.network.toBlockchain()
|
||||
return when {
|
||||
blockchain.isEvm() -> scanEvmTransactionBulk(blockchain, transactionDataList)
|
||||
else -> error("Gas estimation with BlockAid not supported by ${blockchain.fullName}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun scanEvmTransaction(data: TransactionData): CheckTransactionResult =
|
||||
withContext(dispatchers.io) {
|
||||
val response = api.scanJsonRpc(mapper.mapToEvmRequest(data))
|
||||
|
|
@ -41,4 +58,15 @@ internal class DefaultBlockAidRepository(
|
|||
val response = api.scanSolanaMessage(mapper.mapToSolanaRequest(data))
|
||||
mapper.mapToDomain(response)
|
||||
}
|
||||
|
||||
private suspend fun scanEvmTransactionBulk(
|
||||
blockchain: Blockchain,
|
||||
transactionDataList: List<SDKTransactionData.Uncompiled>,
|
||||
): GasEstimationResult = withContext(dispatchers.io) {
|
||||
val response = api.scanEvmTransactionBulk(
|
||||
BlockAidEvmScanTransactionConverter(blockchain).convert(transactionDataList),
|
||||
)
|
||||
|
||||
GasEstimationResponseConverter.convert(response)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.data.blockaid.converters
|
||||
|
||||
import com.domain.blockaid.models.transaction.GasEstimationResult
|
||||
import com.tangem.blockchain.extensions.hexToBigInteger
|
||||
import com.tangem.datasource.api.common.blockaid.models.response.GasEstimationResponse
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object GasEstimationResponseConverter : Converter<List<GasEstimationResponse>, GasEstimationResult> {
|
||||
override fun convert(value: List<GasEstimationResponse>): GasEstimationResult {
|
||||
return GasEstimationResult(
|
||||
estimatedGasList = value.map { it.gasEstimation.estimate.hexToBigInteger() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.data.blockaid.di
|
||||
|
||||
import com.tangem.data.blockaid.DefaultBlockAidGasEstimate
|
||||
import com.tangem.data.blockaid.DefaultBlockAidVerifier
|
||||
import com.tangem.domain.blockaid.BlockAidGasEstimate
|
||||
import com.tangem.domain.blockaid.BlockAidVerifier
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -15,4 +17,8 @@ interface BlockAidDataModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindVerifier(verifier: DefaultBlockAidVerifier): BlockAidVerifier
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindBlockAidGasEstimate(gasEstimate: DefaultBlockAidGasEstimate): BlockAidGasEstimate
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ internal class DefaultCardRepository(
|
|||
override fun wasCardScanned(cardId: String): Flow<Boolean> {
|
||||
return appPreferencesStore.getObjectList<UsedCardInfo>(key = PreferencesKeys.USED_CARDS_INFO_KEY)
|
||||
.map { savedCards ->
|
||||
savedCards?.any { it.cardId == cardId } ?: false
|
||||
savedCards?.any { it.cardId == cardId } == true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,11 +63,11 @@ internal class DefaultCardRepository(
|
|||
}
|
||||
|
||||
override suspend fun isActivationStarted(cardId: String): Boolean {
|
||||
return getUsedCardSync(cardId)?.isActivationStarted ?: false
|
||||
return getUsedCardSync(cardId)?.isActivationStarted == true
|
||||
}
|
||||
|
||||
override suspend fun isActivationFinished(cardId: String): Boolean {
|
||||
return getUsedCardSync(cardId)?.isActivationFinished ?: false
|
||||
return getUsedCardSync(cardId)?.isActivationFinished == true
|
||||
}
|
||||
|
||||
override suspend fun isActivationInProgress(cardId: String): Boolean {
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ suspend inline fun <T> safeApiCallWithTimeout(
|
|||
): T = safeApiCall(
|
||||
call = {
|
||||
withTimeoutOrNull(timeoutMillis) { call() }
|
||||
?: raise(ApiResponseError.TimeoutException)
|
||||
?: raise(ApiResponseError.TimeoutException())
|
||||
},
|
||||
onError = onError,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ internal class DefaultETagsStore(
|
|||
appPreferencesStore.store(key = key, value = value)
|
||||
}
|
||||
|
||||
override suspend fun clear(userWalletId: UserWalletId, key: ETagsStore.Key) {
|
||||
val key = getAccountsETagKey(userWalletId = userWalletId, key = key)
|
||||
appPreferencesStore.editData { it.remove(key) }
|
||||
}
|
||||
|
||||
private fun getAccountsETagKey(userWalletId: UserWalletId, key: ETagsStore.Key): Preferences.Key<String> {
|
||||
return stringPreferencesKey(name = "etag_${key}_${userWalletId.stringValue}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,17 @@ interface ETagsStore {
|
|||
*/
|
||||
suspend fun store(userWalletId: UserWalletId, key: Key, value: String)
|
||||
|
||||
/**
|
||||
* Clears the stored ETag value for the specified wallet and key
|
||||
*
|
||||
* @param userWalletId identifier of the user wallet
|
||||
* @param key the key for which to get the ETag value
|
||||
*/
|
||||
suspend fun clear(userWalletId: UserWalletId, key: Key)
|
||||
|
||||
/** Enumeration of possible keys for storing ETag values */
|
||||
enum class Key {
|
||||
WalletAccounts,
|
||||
UserTokens,
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.data.common.network.NetworkFactory
|
|||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -47,11 +48,35 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
blockchain = blockchain.getTestnetVersion() ?: blockchain
|
||||
}
|
||||
|
||||
val network = networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = responseToken.derivationPath,
|
||||
userWallet = userWallet,
|
||||
) ?: return null
|
||||
|
||||
return createCurrency(responseToken = responseToken, userWallet = userWallet, network = network)
|
||||
}
|
||||
|
||||
fun createCurrency(
|
||||
responseToken: UserTokensResponse.Token,
|
||||
userWallet: UserWallet,
|
||||
network: Network,
|
||||
): CryptoCurrency? {
|
||||
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")
|
||||
return null
|
||||
}
|
||||
|
||||
if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isTestCard()) {
|
||||
blockchain = blockchain.getTestnetVersion() ?: blockchain
|
||||
}
|
||||
|
||||
val sdkToken = createSdkToken(responseToken)
|
||||
return if (sdkToken == null) {
|
||||
createCoin(blockchain, responseToken, userWallet)
|
||||
createCoin(blockchain, responseToken, network)
|
||||
} else {
|
||||
createToken(blockchain, sdkToken, responseToken.derivationPath, userWallet)
|
||||
createToken(blockchain, sdkToken, network)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,14 +95,8 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
private fun createCoin(
|
||||
blockchain: Blockchain,
|
||||
responseToken: UserTokensResponse.Token,
|
||||
userWallet: UserWallet,
|
||||
network: Network,
|
||||
): CryptoCurrency.Coin? {
|
||||
val network = networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = responseToken.derivationPath,
|
||||
userWallet = userWallet,
|
||||
) ?: return null
|
||||
|
||||
return CryptoCurrency.Coin(
|
||||
id = getCoinId(network, blockchain.toCoinId()),
|
||||
network = network,
|
||||
|
|
@ -101,18 +120,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createToken(
|
||||
blockchain: Blockchain,
|
||||
sdkToken: Token,
|
||||
responseDerivationPath: String?,
|
||||
userWallet: UserWallet,
|
||||
): CryptoCurrency.Token? {
|
||||
val network = networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = responseDerivationPath,
|
||||
userWallet = userWallet,
|
||||
) ?: return null
|
||||
|
||||
private fun createToken(blockchain: Blockchain, sdkToken: Token, network: Network): CryptoCurrency.Token? {
|
||||
val id = getTokenId(network, sdkToken)
|
||||
|
||||
return CryptoCurrency.Token(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,24 @@ class NetworkFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create
|
||||
*
|
||||
* @param blockchain blockchain
|
||||
* @param derivationPath derivation path
|
||||
* @param userWallet user wallet
|
||||
*/
|
||||
fun create(blockchain: Blockchain, derivationPath: Network.DerivationPath, userWallet: UserWallet): Network? {
|
||||
return create(
|
||||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
canHandleTokens = userWallet.canHandleToken(
|
||||
blockchain = blockchain,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create
|
||||
*
|
||||
|
|
@ -325,6 +343,9 @@ class NetworkFactory @Inject constructor(
|
|||
Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet,
|
||||
Blockchain.Pepecoin, Blockchain.PepecoinTestnet,
|
||||
Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet,
|
||||
Blockchain.Quai, Blockchain.QuaiTestnet,
|
||||
// Blockchain.Linea, Blockchain.LineaTestnet,
|
||||
// Blockchain.ArbitrumNova,
|
||||
-> Network.TransactionExtrasType.NONE
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ interface QuotesFetcher {
|
|||
value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D).combine(),
|
||||
),
|
||||
LAST_UPDATED_AT(value = "lastUpdatedAt"),
|
||||
;
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
|
|
|
|||
|
|
@ -440,5 +440,42 @@ internal class DefaultQuotesFetcherTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if not all quotes are fetched`() = runTest {
|
||||
// Arrange
|
||||
val apiQuote = MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE)
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = QuotesResponse(quotes = mapOf("ethereum" to apiQuote)),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getQuotes(currencyId = "usd", coinIds = "ethereum,bitcoin", fields = "price")
|
||||
} returns apiResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher.fetch(
|
||||
fiatCurrencyId = "usd",
|
||||
currenciesIds = setOf("ethereum", "bitcoin"),
|
||||
fields = setOf(Field.PRICE),
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = QuotesResponse(
|
||||
quotes = mapOf(
|
||||
"ethereum" to apiQuote,
|
||||
"bitcoin" to QuotesResponse.Quote.EMPTY,
|
||||
),
|
||||
).right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
tangemTechApi.getQuotes(
|
||||
currencyId = "usd",
|
||||
coinIds = "ethereum,bitcoin",
|
||||
fields = "price",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Iterable<QuoteMetadata>?.toResponseQuotes() = this!!.associate { it.cryptoCurrencyId to it.value }
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import com.tangem.data.feedback.converters.BlockchainInfoConverter
|
|||
import com.tangem.data.feedback.converters.WalletMetaInfoConverter
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.feedback.models.*
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.data.feedback.DefaultFeedbackFeatureToggles
|
|||
import com.tangem.data.feedback.DefaultFeedbackRepository
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ internal class DefaultCustomTokensRepository(
|
|||
private val networkFactory: NetworkFactory,
|
||||
) : CustomTokensRepository {
|
||||
|
||||
private val excludedBlockchainsForCustom = setOf(
|
||||
Blockchain.Quai,
|
||||
Blockchain.QuaiTestnet,
|
||||
)
|
||||
|
||||
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
|
||||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
private val tokenAddressConverter = TokenAddressesConverter()
|
||||
|
|
@ -268,6 +273,7 @@ internal class DefaultCustomTokensRepository(
|
|||
val scanResponse = userWallet.scanResponse
|
||||
|
||||
Blockchain.entries
|
||||
.filter { it !in excludedBlockchainsForCustom }
|
||||
.mapNotNull { blockchain ->
|
||||
val canHandleBlockchain = scanResponse.card.canHandleBlockchain(
|
||||
blockchain,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ internal object TokenMarketListConverter : Converter<TokenMarketListResponse, To
|
|||
),
|
||||
tokenCharts = TokenMarket.Charts(h24 = null, week = null, month = null),
|
||||
stakingRate = stakingRate,
|
||||
updateTimestamp = value.timestamp,
|
||||
)
|
||||
}
|
||||
return TokenMarketListWithMaxApy(tokens, value.summary?.maxApy)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ dependencies {
|
|||
|
||||
// region Project - Libs
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(tangemDeps.blockchain) { exclude(module = "joda-time") }
|
||||
// endregion
|
||||
|
||||
// region DI
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -2,45 +2,45 @@ 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,
|
||||
effectiveProtocolBalance = it.effectiveProtocolBalance,
|
||||
)
|
||||
|
||||
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,
|
||||
effectiveProtocolBalance = yieldSupplyStatus.effectiveProtocolBalance,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,14 +58,11 @@ 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,
|
||||
effectiveProtocolBalance = BigDecimal.ONE,
|
||||
),
|
||||
ID(
|
||||
prefix = Prefix.COIN_PREFIX,
|
||||
|
|
@ -79,25 +73,29 @@ 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,
|
||||
effectiveProtocolBalance = BigDecimal.ONE,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -120,17 +118,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,
|
||||
|
|
|
|||
|
|
@ -2,82 +2,77 @@ 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
|
||||
import java.math.BigDecimal
|
||||
|
||||
@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,
|
||||
effectiveProtocolBalance = BigDecimal.ONE,
|
||||
)
|
||||
|
||||
@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,
|
||||
effectiveProtocolBalance = BigDecimal.ONE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,29 @@ 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,
|
||||
effectiveProtocolBalance = BigDecimal.ONE,
|
||||
),
|
||||
"coin⟨ETH⟩ethereum" to null,
|
||||
),
|
||||
),
|
||||
expected = SimpleNetworkStatus(
|
||||
|
|
@ -101,33 +95,17 @@ 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,
|
||||
effectiveProtocolBalance = BigDecimal.ONE,
|
||||
),
|
||||
ID(
|
||||
prefix = Prefix.COIN_PREFIX,
|
||||
body = Body.NetworkId(rawId = "ETH"),
|
||||
suffix = ID.Suffix.RawID(rawId = "ethereum"),
|
||||
) to null,
|
||||
),
|
||||
source = StatusSource.CACHE,
|
||||
),
|
||||
|
|
@ -137,21 +115,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 +167,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 +254,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 +275,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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -272,6 +272,7 @@ internal class NetworkStatusFactoryTest(private val model: Model) {
|
|||
isActive = false,
|
||||
isInitialized = false,
|
||||
isAllowedToSpend = false,
|
||||
effectiveProtocolBalance = BigDecimal.ONE,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ package com.tangem.data.notifications
|
|||
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
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.datasource.local.preferences.PreferencesKeys.getShouldShowAskNotificationPermissionViaBs
|
||||
import com.tangem.datasource.local.preferences.utils.*
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultNotificationsRepository @Inject constructor(
|
||||
|
|
@ -17,6 +16,10 @@ class DefaultNotificationsRepository @Inject constructor(
|
|||
return appPreferencesStore.getSyncOrDefault(PreferencesKeys.getShouldShowNotificationKey(key), true)
|
||||
}
|
||||
|
||||
override fun getShouldShowNotification(key: String): Flow<Boolean> {
|
||||
return appPreferencesStore.get(PreferencesKeys.getShouldShowNotificationKey(key), true)
|
||||
}
|
||||
|
||||
override suspend fun setShouldShowNotifications(key: String, value: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getShouldShowNotificationKey(key), value)
|
||||
}
|
||||
|
|
@ -61,12 +64,22 @@ class DefaultNotificationsRepository @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun setNotificationsWasEnabledAutomatically(userWalletId: String) {
|
||||
appPreferencesStore.editData {
|
||||
it.setObjectMap(
|
||||
appPreferencesStore.editData { preferences ->
|
||||
preferences.setObjectMap(
|
||||
key = PreferencesKeys.NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY,
|
||||
value = it.getObjectMap<Boolean>(PreferencesKeys.NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY)
|
||||
value = preferences.getObjectMap<Boolean>(
|
||||
PreferencesKeys.NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY,
|
||||
)
|
||||
.plus(userWalletId to true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shouldAskNotificationPermissionsViaBs(): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(getShouldShowAskNotificationPermissionViaBs(), false)
|
||||
}
|
||||
|
||||
override suspend fun setShouldAskNotificationPermissionsViaBs(shouldAsk: Boolean) {
|
||||
appPreferencesStore.store(key = getShouldShowAskNotificationPermissionViaBs(), value = shouldAsk)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ internal class DefaultPushNotificationsRepository @Inject constructor(
|
|||
) : PushNotificationsRepository {
|
||||
|
||||
override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) {
|
||||
tangemTechApi.createApplicationId(
|
||||
val appId = tangemTechApi.createApplicationId(
|
||||
NotificationApplicationCreateBody(
|
||||
platform = appInfoProvider.platform.lowercase(),
|
||||
device = appInfoProvider.device,
|
||||
|
|
@ -33,7 +33,9 @@ internal class DefaultPushNotificationsRepository @Inject constructor(
|
|||
version = appInfoProvider.appVersion,
|
||||
pushToken = pushToken,
|
||||
),
|
||||
).getOrThrow().appId.let(::ApplicationId)
|
||||
).getOrThrow().appId
|
||||
|
||||
ApplicationId(appId)
|
||||
}
|
||||
|
||||
override suspend fun saveApplicationId(appId: ApplicationId) {
|
||||
|
|
|
|||
|
|
@ -159,5 +159,8 @@ public val Blockchain.mercuryoNetwork: String?
|
|||
Blockchain.KaspaTestnet -> null
|
||||
Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> null
|
||||
Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> null
|
||||
Blockchain.Quai, Blockchain.QuaiTestnet -> null
|
||||
// Blockchain.Linea, Blockchain.LineaTestnet -> null
|
||||
// Blockchain.ArbitrumNova -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -50,16 +50,16 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
|
|||
val result = QrResult(address = address)
|
||||
|
||||
extractParameters(withoutSchema)
|
||||
.forEach {
|
||||
when (it.key) {
|
||||
.forEach { entry ->
|
||||
when (entry.key) {
|
||||
Parameter.Amount -> {
|
||||
// According to BIP-0021, the value is specified in decimals. No conversion needed
|
||||
result.amount = it.value.parseBigDecimalOrNull()
|
||||
result.amount = entry.value.parseBigDecimalOrNull()
|
||||
}
|
||||
Parameter.Message,
|
||||
Parameter.Memo,
|
||||
-> {
|
||||
result.memo = URLDecoder.decode(it.value, "UTF-8")
|
||||
result.memo = URLDecoder.decode(entry.value, "UTF-8")
|
||||
}
|
||||
Parameter.Address -> {
|
||||
// If 'address' parameter is exists, then currency must be TOKEN.
|
||||
|
|
@ -70,7 +70,7 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
|
|||
// matches the contract address of the token.
|
||||
// Otherwise, the scanned string is likely malformed, and we stop the entire parsing routin
|
||||
if (tokenCurrency.contractAddress.equals(address, ignoreCase = true)) {
|
||||
result.address = it.value
|
||||
result.address = entry.value
|
||||
} else {
|
||||
return QrResult()
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
|
|||
-> {
|
||||
// Extra convert parses scientific notation to decimal
|
||||
// This is necessary to be able comparing BigDecimal values
|
||||
result.amount = it.value.parseBigDecimalOrNull()
|
||||
result.amount = entry.value.parseBigDecimalOrNull()
|
||||
?.toPlainString()?.toBigDecimalOrNull()
|
||||
?.divide(BigDecimal.TEN.pow(cryptoCurrency.decimals))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null)
|
||||
|
||||
val currenciesIds = setOf("BTC", "ETH")
|
||||
val error = QuotesFetcher.Error.ApiOperationError(ApiResponseError.NetworkException)
|
||||
val error = QuotesFetcher.Error.ApiOperationError(ApiResponseError.NetworkException())
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
|
|
@ -173,7 +173,9 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException("Cause: ApiOperationError(apiError=NetworkException)").left()
|
||||
val expected = IllegalStateException(
|
||||
"Cause: ApiOperationError(apiError=com.tangem.datasource.api.common.response.ApiResponseError\$NetworkException)",
|
||||
).left()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
|
|
@ -215,50 +217,6 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if not all quotes are fetched`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null)
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
|
||||
val currenciesIds = setOf("BTC", "ETH")
|
||||
val response = QuotesResponse(
|
||||
quotes = mapOf(
|
||||
"BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
|
||||
} returns response.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
val storedQuotes = QuotesResponse(
|
||||
quotes = mapOf(
|
||||
"BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE),
|
||||
"ETH" to QuotesResponse.Quote.EMPTY,
|
||||
),
|
||||
)
|
||||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
|
||||
quotesStore.store(values = storedQuotes.quotes)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val currenciesIds = setOf(
|
||||
|
|
|
|||
|
|
@ -30,13 +30,22 @@ internal class DefaultAppRatingRepository(
|
|||
override fun isReadyToShow(): Flow<Boolean> {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
return combine(
|
||||
appPreferencesStore.get(key = PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY, default = false),
|
||||
appPreferencesStore.get(
|
||||
flow = appPreferencesStore.get(
|
||||
key = PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY,
|
||||
default = false,
|
||||
),
|
||||
flow2 = appPreferencesStore.get(
|
||||
key = PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY,
|
||||
default = FIRST_SHOWING_COUNT,
|
||||
),
|
||||
appPreferencesStore.get(key = PreferencesKeys.APP_LAUNCH_COUNT_KEY, default = DEFAULT_APP_LAUNCH_COUNT),
|
||||
appPreferencesStore.get(key = PreferencesKeys.FUNDS_FOUND_DATE_KEY, default = FUNDS_FOUND_DATE_UNDEFINED),
|
||||
flow3 = appPreferencesStore.get(
|
||||
key = PreferencesKeys.APP_LAUNCH_COUNT_KEY,
|
||||
default = DEFAULT_APP_LAUNCH_COUNT,
|
||||
),
|
||||
flow4 = appPreferencesStore.get(
|
||||
key = PreferencesKeys.FUNDS_FOUND_DATE_KEY,
|
||||
default = FUNDS_FOUND_DATE_UNDEFINED,
|
||||
),
|
||||
) { isInteracting, ratingShowingCount, appLaunchCount, fundsFoundDate ->
|
||||
if (!isInteracting) {
|
||||
val diff = Calendar.getInstance().timeInMillis - fundsFoundDate
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.tangem.datasource.api.common.response.ApiResponse
|
|||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.request.*
|
||||
import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction
|
||||
|
|
@ -56,6 +57,8 @@ import com.tangem.lib.crypto.BlockchainUtils.isCardano
|
|||
import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
|
@ -91,17 +94,20 @@ internal class DefaultStakingRepository(
|
|||
|
||||
override suspend fun fetchEnabledYields() {
|
||||
withContext(dispatchers.io) {
|
||||
when (val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)) {
|
||||
is ApiResponse.Success -> stakingYieldsStore.store(
|
||||
stakingTokensWithYields.data.data.filter {
|
||||
it.isAvailable == true
|
||||
},
|
||||
)
|
||||
else -> {
|
||||
stakingYieldsStore.store(emptyList())
|
||||
throw (stakingTokensWithYields as ApiResponse.Error).cause
|
||||
val yieldsResponses = getAvailableIntegrationsIds().map {
|
||||
async { it.getYieldRequest() }
|
||||
}.awaitAll()
|
||||
|
||||
val yields = yieldsResponses.flatMap { response ->
|
||||
when (response) {
|
||||
is ApiResponse.Success -> response.data.data.filter { yield -> yield.isAvailable == true }
|
||||
else -> {
|
||||
Timber.e("Error fetching enabled yields: ${(response as? ApiResponse.Error)?.cause}")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
stakingYieldsStore.store(yields)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +157,26 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun StakingIntegrationID.getYieldRequest(): ApiResponse<EnabledYieldsResponse> {
|
||||
return when (this) {
|
||||
is StakingIntegrationID.Coin -> stakeKitApi.getEnabledYields(
|
||||
preferredValidatorsOnly = false,
|
||||
network = networkId,
|
||||
)
|
||||
is StakingIntegrationID.EthereumToken -> stakeKitApi.getEnabledYields(
|
||||
preferredValidatorsOnly = false,
|
||||
yieldId = value,
|
||||
network = networkId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAvailableIntegrationsIds(): List<StakingIntegrationID> {
|
||||
return StakingIntegrationID.entries.filterNot {
|
||||
it.blockchain == Blockchain.Cardano && !stakingFeatureToggles.isCardanoStakingEnabled
|
||||
}
|
||||
}
|
||||
|
||||
private fun NetworkTypeDTO.extractJsonName(): String {
|
||||
return networkTypeAdapter.toJson(this).replace("\"", "")
|
||||
}
|
||||
|
|
@ -164,7 +190,11 @@ internal class DefaultStakingRepository(
|
|||
val yield = getYield(cryptoCurrencyId, symbol)
|
||||
|
||||
StakingEntryInfo(
|
||||
apr = requireNotNull(yield.preferredValidators.maxByOrNull { it.apr.orZero() }?.apr),
|
||||
rewardInfo = requireNotNull(
|
||||
yield
|
||||
.preferredValidators
|
||||
.maxByOrNull { it.rewardInfo?.rate.orZero() }?.rewardInfo,
|
||||
),
|
||||
rewardSchedule = yield.metadata.rewardSchedule,
|
||||
tokenSymbol = yield.token.symbol,
|
||||
)
|
||||
|
|
@ -360,7 +390,8 @@ internal class DefaultStakingRepository(
|
|||
)
|
||||
|
||||
val transaction = transactionConverter.convert(transactionResponse.getOrThrow())
|
||||
val unsignedTransaction = transaction.unsignedTransaction ?: error("No unsigned transaction available")
|
||||
val unsignedTransaction =
|
||||
transaction.unsignedTransaction ?: error("No unsigned transaction available")
|
||||
val transactionData = TransactionData.Compiled(
|
||||
value = getTransactionDataType(networkId, unsignedTransaction),
|
||||
fee = fee,
|
||||
|
|
@ -469,7 +500,7 @@ internal class DefaultStakingRepository(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getEnabledYields(): Flow<List<Yield>> {
|
||||
override fun getEnabledYields(): Flow<List<Yield>> {
|
||||
return stakingYieldsStore.get().map {
|
||||
YieldConverter.convertListIgnoreErrors(
|
||||
input = it,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import com.tangem.domain.staking.model.stakekit.Yield
|
|||
import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule
|
||||
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal object YieldConverter : Converter<YieldDTO, Yield> {
|
||||
|
||||
|
|
@ -21,7 +23,10 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
|||
|
||||
private val PARTNERS_NAMES = listOf("Meria")
|
||||
|
||||
private const val DIVIDE_SCALE = 8
|
||||
|
||||
override fun convert(value: YieldDTO): Yield {
|
||||
val rewardType = convertRewardType(value.rewardType.asMandatory("rewardType"))
|
||||
return Yield(
|
||||
id = value.id.asMandatory("id"),
|
||||
token = YieldTokenConverter.convert(value.token.asMandatory("token")),
|
||||
|
|
@ -30,14 +35,14 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
|||
status = convertStatus(value.status.asMandatory("status")),
|
||||
apy = value.apy.asMandatory("apy"),
|
||||
rewardRate = value.rewardRate.asMandatory("rewardRate"),
|
||||
rewardType = convertRewardType(value.rewardType.asMandatory("rewardType")),
|
||||
rewardType = rewardType,
|
||||
metadata = convertMetadata(value.metadata.asMandatory("metadata")),
|
||||
validators = value.validators.asMandatory("validators")
|
||||
.asSequence()
|
||||
.distinctBy { it.address }
|
||||
.filter { it.status == ValidatorStatusDTO.ACTIVE }
|
||||
.map { convertValidator(it) }
|
||||
.sortedByDescending { it.apr }
|
||||
.map { convertValidator(validatorDTO = it, rewardType = rewardType) }
|
||||
.sortedByDescending { it.rewardInfo?.rate?.orZero() }
|
||||
.sortedByDescending { it.isStrategicPartner }
|
||||
.toImmutableList(),
|
||||
isAvailable = value.isAvailable.asMandatory("isAvailable"),
|
||||
|
|
@ -119,15 +124,16 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
|||
)
|
||||
}
|
||||
|
||||
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator {
|
||||
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO, rewardType: Yield.RewardType): Yield.Validator {
|
||||
val address = validatorDTO.address.asMandatory("address")
|
||||
|
||||
return Yield.Validator(
|
||||
address = address,
|
||||
status = convertValidatorStatus(validatorDTO.status.asMandatory("status")),
|
||||
name = validatorDTO.name.asMandatory("name"),
|
||||
image = validatorDTO.image,
|
||||
website = validatorDTO.website,
|
||||
apr = validatorDTO.apr,
|
||||
rewardInfo = createRewardInfo(validatorDTO, rewardType),
|
||||
commission = validatorDTO.commission,
|
||||
stakedBalance = validatorDTO.stakedBalance,
|
||||
votingPower = validatorDTO.votingPower,
|
||||
|
|
@ -136,6 +142,32 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createRewardInfo(validatorDTO: YieldDTO.ValidatorDTO, rewardType: Yield.RewardType): Yield.RewardInfo? {
|
||||
val aprOrApy = validatorDTO.apr
|
||||
val commission = validatorDTO.commission
|
||||
// gross = net / (1 - commission)
|
||||
return try {
|
||||
val netApy = aprOrApy
|
||||
val grossAprOrApy = if (netApy != null && commission != null) {
|
||||
val commissionFraction = commission.toBigDecimal()
|
||||
if (commissionFraction < 1.toBigDecimal()) {
|
||||
netApy.divide(
|
||||
1.toBigDecimal() - commissionFraction,
|
||||
DIVIDE_SCALE,
|
||||
RoundingMode.HALF_UP,
|
||||
)
|
||||
} else {
|
||||
netApy
|
||||
}
|
||||
} else {
|
||||
netApy
|
||||
}
|
||||
grossAprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) }
|
||||
} catch (_: Exception) {
|
||||
aprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): Yield.RewardType {
|
||||
return when (rewardTypeDTO) {
|
||||
YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue