Updated on 2026-08-14
This commit is contained in:
commit
9ba66ba084
179 changed files with 5757 additions and 1156 deletions
|
|
@ -9,9 +9,14 @@ android {
|
|||
namespace = "com.tangem.data.account"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region Project - Core
|
||||
implementation(projects.core.datasource)
|
||||
api(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
|
|
@ -20,18 +25,33 @@ dependencies {
|
|||
api(projects.domain.models)
|
||||
// endregion
|
||||
|
||||
// Project - Data
|
||||
implementation(projects.core.datasource)
|
||||
// region Project - Data
|
||||
implementation(projects.data.common)
|
||||
// endregion
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.core)
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region AndroidX libraries
|
||||
implementation(deps.androidx.datastore)
|
||||
// endregion
|
||||
|
||||
// region Other Dependencies
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
// endregion
|
||||
|
||||
// region Test
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Container for converter factories related to accounts.
|
||||
*
|
||||
* @property accountsListCF factory for creating an account list converter
|
||||
* @property getWalletAccountsResponseCF factory for creating a wallet accounts response converter
|
||||
* @property cryptoPortfolioCF factory for creating a crypto portfolio converter
|
||||
*
|
||||
* @constructor Creates an instance of the container with injected factories.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class AccountConverterFactoryContainer @Inject constructor(
|
||||
val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory,
|
||||
private val accountsListCF: AccountListConverter.Factory,
|
||||
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
) {
|
||||
|
||||
fun createAccountListConverter(userWalletId: UserWalletId): AccountListConverter {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
|
||||
|
||||
return accountsListCF.create(userWallet)
|
||||
}
|
||||
|
||||
fun createCryptoPortfolioConverter(userWalletId: UserWalletId): CryptoPortfolioConverter {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
|
||||
|
||||
return cryptoPortfolioCF.create(userWallet)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
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.UserWalletId
|
||||
|
||||
internal fun String.toAccountId(userWalletId: UserWalletId): AccountId {
|
||||
return AccountId.forCryptoPortfolio(value = this, userWalletId = userWalletId).getOrElse {
|
||||
error("Unable to create AccountId from value: $this. Cause: $it")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun String.toAccountName(): AccountName {
|
||||
return AccountName(value = this).getOrElse {
|
||||
error("Unable to create AccountName from value: $this. Cause: $it")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletAccountDTO.toIcon(): CryptoPortfolioIcon {
|
||||
return CryptoPortfolioIconConverter.convert(
|
||||
value = CryptoPortfolioIconConverter.DataModel(icon = icon, color = iconColor),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun Int.toDerivationIndex(): DerivationIndex {
|
||||
return DerivationIndex(value = this).getOrElse {
|
||||
error("Unable to create DerivationIndex from value: $this. Cause: $it")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.utils.converter.Converter
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
/**
|
||||
* Converts a [GetWalletAccountsResponse] to an [AccountList] and vice versa
|
||||
*
|
||||
* @property userWallet the user wallet associated with the account list
|
||||
* @param cryptoPortfolioConverterFactory factory to create [CryptoPortfolioConverter] instances
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class AccountListConverter @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
cryptoPortfolioConverterFactory: CryptoPortfolioConverter.Factory,
|
||||
) : Converter<GetWalletAccountsResponse, AccountList> {
|
||||
|
||||
private val cryptoPortfolioConverter: CryptoPortfolioConverter by lazy {
|
||||
cryptoPortfolioConverterFactory.create(userWallet)
|
||||
}
|
||||
|
||||
override fun convert(value: GetWalletAccountsResponse): AccountList {
|
||||
return AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = value.accounts.map(cryptoPortfolioConverter::convert).toSet(),
|
||||
totalAccounts = value.wallet.totalAccounts,
|
||||
sortType = TokensSortTypeConverter.convert(value.wallet.sort),
|
||||
groupType = TokensGroupTypeConverter.convert(value.wallet.group),
|
||||
)
|
||||
.getOrElse {
|
||||
error("Failed to convert GetWalletAccountsResponse to AccountList: $it")
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(userWallet: UserWallet): AccountListConverter
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converts a [WalletAccountDTO] to an [ArchivedAccount]
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet associated with the account
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ArchivedAccountConverter(
|
||||
private val userWalletId: UserWalletId,
|
||||
) : Converter<WalletAccountDTO, ArchivedAccount> {
|
||||
|
||||
override fun convert(value: WalletAccountDTO): ArchivedAccount {
|
||||
return ArchivedAccount(
|
||||
accountId = value.id.toAccountId(userWalletId = userWalletId),
|
||||
name = value.name.toAccountName(),
|
||||
icon = value.toIcon(),
|
||||
derivationIndex = value.derivationIndex.toDerivationIndex(),
|
||||
tokensCount = value.totalTokens ?: error("Total tokens should not be null"),
|
||||
networksCount = value.totalNetworks ?: error("Total networks should not be null"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
/**
|
||||
* Converts a [WalletAccountDTO] to an [Account.CryptoPortfolio] and vise versa
|
||||
*
|
||||
* @property userWallet the user wallet associated with the account list
|
||||
* @property responseCryptoCurrenciesFactory factory to create crypto currencies from response tokens
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class CryptoPortfolioConverter @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
private val userTokensResponseFactory: UserTokensResponseFactory,
|
||||
) : TwoWayConverter<WalletAccountDTO, Account.CryptoPortfolio> {
|
||||
|
||||
override fun convert(value: WalletAccountDTO): Account.CryptoPortfolio {
|
||||
val tokens = value.tokens ?: error("Tokens should not be null")
|
||||
|
||||
return Account.CryptoPortfolio(
|
||||
accountId = value.id.toAccountId(userWallet.walletId),
|
||||
accountName = value.name.toAccountName(),
|
||||
icon = value.toIcon(),
|
||||
derivationIndex = value.derivationIndex.toDerivationIndex(),
|
||||
cryptoCurrencies = if (tokens.isNotEmpty()) {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = tokens,
|
||||
userWallet = userWallet,
|
||||
).toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertBack(value: Account.CryptoPortfolio): WalletAccountDTO {
|
||||
return WalletAccountDTO(
|
||||
id = value.accountId.value,
|
||||
name = value.accountName.value,
|
||||
derivationIndex = value.derivationIndex.value,
|
||||
icon = value.icon.value.name,
|
||||
iconColor = value.icon.color.name,
|
||||
tokens = value.cryptoCurrencies.map(userTokensResponseFactory::createResponseToken),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(userWallet: UserWallet): CryptoPortfolioConverter
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converts a [CryptoPortfolioIconConverter.DataModel] to a [CryptoPortfolioIcon]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object CryptoPortfolioIconConverter : Converter<CryptoPortfolioIconConverter.DataModel, CryptoPortfolioIcon> {
|
||||
|
||||
override fun convert(value: DataModel): CryptoPortfolioIcon {
|
||||
return CryptoPortfolioIcon.ofCustomAccount(
|
||||
value = CryptoPortfolioIcon.Icon.valueOf(value.icon),
|
||||
color = CryptoPortfolioIcon.Color.valueOf(value.color),
|
||||
)
|
||||
}
|
||||
|
||||
data class DataModel(val icon: String, val color: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.utils.converter.Converter
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class GetWalletAccountsResponseConverter @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted val version: Int,
|
||||
cryptoPortfolioConverterFactory: CryptoPortfolioConverter.Factory,
|
||||
) : Converter<AccountList, GetWalletAccountsResponse> {
|
||||
|
||||
private val cryptoPortfolioConverter: CryptoPortfolioConverter by lazy {
|
||||
cryptoPortfolioConverterFactory.create(userWallet)
|
||||
}
|
||||
|
||||
override fun convert(value: AccountList): GetWalletAccountsResponse {
|
||||
return GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = version,
|
||||
group = TokensGroupTypeConverter.convertBack(value.groupType),
|
||||
sort = TokensSortTypeConverter.convertBack(value.sortType),
|
||||
totalAccounts = value.totalAccounts,
|
||||
),
|
||||
accounts = value.accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.map(cryptoPortfolioConverter::convertBack),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(userWallet: UserWallet, version: Int): GetWalletAccountsResponseConverter
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
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
|
||||
|
||||
/**
|
||||
* Converts an [AccountList] to a [SaveWalletAccountsResponse]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object SaveWalletAccountsResponseConverter : Converter<AccountList, SaveWalletAccountsResponse> {
|
||||
|
||||
override fun convert(value: AccountList): SaveWalletAccountsResponse {
|
||||
return SaveWalletAccountsResponse(
|
||||
accounts = value.accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.map(::toDTO),
|
||||
)
|
||||
}
|
||||
|
||||
private fun toDTO(account: Account.CryptoPortfolio): WalletAccountDTO {
|
||||
return WalletAccountDTO(
|
||||
id = account.accountId.value,
|
||||
name = account.accountName.value,
|
||||
derivationIndex = account.derivationIndex.value,
|
||||
icon = account.icon.value.name,
|
||||
iconColor = account.icon.color.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.TokensGroupType
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
|
||||
/**
|
||||
* Converts a [UserTokensResponse.GroupType] to a [TokensGroupType] and vice versa
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object TokensGroupTypeConverter : TwoWayConverter<UserTokensResponse.GroupType, TokensGroupType> {
|
||||
|
||||
override fun convert(value: UserTokensResponse.GroupType): TokensGroupType {
|
||||
return when (value) {
|
||||
UserTokensResponse.GroupType.NETWORK -> TokensGroupType.NETWORK
|
||||
UserTokensResponse.GroupType.NONE,
|
||||
UserTokensResponse.GroupType.TOKEN,
|
||||
-> TokensGroupType.NONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: TokensGroupType): UserTokensResponse.GroupType {
|
||||
return when (value) {
|
||||
TokensGroupType.NONE -> UserTokensResponse.GroupType.NONE
|
||||
TokensGroupType.NETWORK -> UserTokensResponse.GroupType.NETWORK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
|
||||
/**
|
||||
* Converts a [UserTokensResponse.SortType] to a [TokensSortType] and vice versa
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object TokensSortTypeConverter : TwoWayConverter<UserTokensResponse.SortType, TokensSortType> {
|
||||
|
||||
override fun convert(value: UserTokensResponse.SortType): TokensSortType {
|
||||
return when (value) {
|
||||
UserTokensResponse.SortType.BALANCE -> TokensSortType.BALANCE
|
||||
UserTokensResponse.SortType.MANUAL,
|
||||
UserTokensResponse.SortType.MARKETCAP,
|
||||
-> TokensSortType.NONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: TokensSortType): UserTokensResponse.SortType {
|
||||
return when (value) {
|
||||
TokensSortType.NONE -> UserTokensResponse.SortType.MANUAL
|
||||
TokensSortType.BALANCE -> UserTokensResponse.SortType.BALANCE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import com.tangem.data.account.converter.AccountConverterFactoryContainer
|
||||
import com.tangem.data.account.repository.DefaultAccountsCRUDRepository
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -16,10 +20,20 @@ internal object AccountDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountsCRUDRepository(userWalletsStore: UserWalletsStore): AccountsCRUDRepository {
|
||||
fun provideAccountsCRUDRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
accountConverterFactoryContainer: AccountConverterFactoryContainer,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AccountsCRUDRepository {
|
||||
return DefaultAccountsCRUDRepository(
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
tangemTechApi = tangemTechApi,
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
archivedAccountsStoreFactory = ArchivedAccountsStoreFactory,
|
||||
userWalletsStore = userWalletsStore,
|
||||
convertersContainer = accountConverterFactoryContainer,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +1,150 @@
|
|||
package com.tangem.data.account.repository
|
||||
|
||||
import arrow.core.Option
|
||||
import arrow.core.Option.Companion.catch
|
||||
import arrow.core.none
|
||||
import arrow.core.raise.option
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import arrow.core.toOption
|
||||
import com.tangem.data.account.converter.AccountConverterFactoryContainer
|
||||
import com.tangem.data.account.converter.ArchivedAccountConverter
|
||||
import com.tangem.data.account.converter.SaveWalletAccountsResponseConverter
|
||||
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.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
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.account.models.ArchivedAccount
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.models.account.*
|
||||
import com.tangem.domain.models.account.Account
|
||||
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.extensions.addOrReplace
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: [REDACTED_JIRA]
|
||||
internal class DefaultAccountsCRUDRepository(
|
||||
private val runtimeStore: RuntimeSharedStore<List<AccountList>>,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val convertersContainer: AccountConverterFactoryContainer,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : AccountsCRUDRepository {
|
||||
|
||||
override suspend fun getAccounts(userWalletId: UserWalletId): Option<AccountList> = catch {
|
||||
runtimeStore.getSyncOrNull()
|
||||
?.firstOrNull { it.userWallet.walletId == userWalletId }
|
||||
?: return none()
|
||||
private val saveAccountsMutex = Mutex()
|
||||
|
||||
override suspend fun getAccountListSync(userWalletId: UserWalletId): Option<AccountList> = option {
|
||||
val accountListResponse = getAccountsResponseSync(userWalletId = userWalletId)
|
||||
|
||||
ensureNotNull(accountListResponse)
|
||||
|
||||
val converter = convertersContainer.createAccountListConverter(userWalletId = userWalletId)
|
||||
converter.convert(value = accountListResponse)
|
||||
}
|
||||
|
||||
override suspend fun getAccount(accountId: AccountId): Option<Account.CryptoPortfolio> = catch {
|
||||
runtimeStore.getSyncOrNull().orEmpty()
|
||||
.flatMap { it.accounts }
|
||||
.firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio
|
||||
?: return none()
|
||||
override suspend fun getAccountSync(accountId: AccountId): Option<Account.CryptoPortfolio> = option {
|
||||
val userWalletId = accountId.userWalletId
|
||||
|
||||
val accountResponse = getAccountsResponseSync(userWalletId = userWalletId)
|
||||
?.accounts?.firstOrNull { it.id == accountId.value }
|
||||
|
||||
ensureNotNull(accountResponse)
|
||||
|
||||
val converter = convertersContainer.createCryptoPortfolioConverter(userWalletId = userWalletId)
|
||||
converter.convert(value = accountResponse)
|
||||
}
|
||||
|
||||
override suspend fun getArchivedAccount(accountId: AccountId): Option<ArchivedAccount> = option {
|
||||
createMockArchivedAccount(userWalletId = accountId.userWalletId)
|
||||
override suspend fun getArchivedAccountSync(accountId: AccountId): Option<ArchivedAccount> {
|
||||
val store = getArchivedAccountsStore(userWalletId = accountId.userWalletId)
|
||||
|
||||
return store.getSyncOrNull()
|
||||
?.firstOrNull { it.accountId == accountId }
|
||||
.toOption()
|
||||
}
|
||||
|
||||
override suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option<List<ArchivedAccount>> = option {
|
||||
listOf(
|
||||
createMockArchivedAccount(userWalletId),
|
||||
)
|
||||
override suspend fun getArchivedAccountListSync(userWalletId: UserWalletId): Option<List<ArchivedAccount>> {
|
||||
val store = getArchivedAccountsStore(userWalletId = userWalletId)
|
||||
|
||||
return store.getSyncOrNull().toOption()
|
||||
}
|
||||
|
||||
override fun getArchivedAccounts(userWalletId: UserWalletId): Flow<List<ArchivedAccount>> {
|
||||
return flow {
|
||||
getArchivedAccountsSync(userWalletId).getOrNull().orEmpty()
|
||||
}
|
||||
val store = getArchivedAccountsStore(userWalletId = userWalletId)
|
||||
|
||||
return store.get()
|
||||
}
|
||||
|
||||
override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) = Unit
|
||||
override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) {
|
||||
val response = withContext(dispatchers.io) {
|
||||
tangemTechApi.getWalletArchivedAccounts(walletId = userWalletId.stringValue).getOrThrow()
|
||||
}
|
||||
|
||||
val store = getArchivedAccountsStore(userWalletId = userWalletId)
|
||||
val converter = ArchivedAccountConverter(userWalletId = userWalletId)
|
||||
|
||||
val archivedAccounts = converter.convertList(input = response.accounts)
|
||||
|
||||
store.store(value = archivedAccounts)
|
||||
}
|
||||
|
||||
override suspend fun saveAccounts(accountList: AccountList) {
|
||||
runtimeStore.update(emptyList()) {
|
||||
it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId }
|
||||
saveAccountsMutex.withLock {
|
||||
val store = getAccountsResponseStore(userWalletId = accountList.userWallet.walletId)
|
||||
|
||||
val version = store.data.firstOrNull()?.wallet?.version ?: 0
|
||||
val body = SaveWalletAccountsResponseConverter.convert(value = accountList)
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = accountList.userWallet.walletId.stringValue,
|
||||
ifMatch = version.toString(),
|
||||
body = body,
|
||||
)
|
||||
.getOrThrow()
|
||||
}
|
||||
|
||||
val converter = convertersContainer.getWalletAccountsResponseCF.create(
|
||||
userWallet = accountList.userWallet,
|
||||
version = version,
|
||||
)
|
||||
|
||||
val accountsResponse = converter.convert(value = accountList)
|
||||
|
||||
store.updateData { accountsResponse }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int {
|
||||
val activeAccountsCount = runtimeStore.getSyncOrNull()?.size ?: 1
|
||||
override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Option<Int> = option {
|
||||
val accountListResponse = getAccountsResponseSync(userWalletId = userWalletId)
|
||||
|
||||
return activeAccountsCount + 1
|
||||
ensureNotNull(accountListResponse)
|
||||
|
||||
return accountListResponse.wallet.totalAccounts.toOption()
|
||||
}
|
||||
|
||||
override fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
return userWalletsStore.getSyncStrict(userWalletId)
|
||||
}
|
||||
|
||||
private fun createMockArchivedAccount(userWalletId: UserWalletId): ArchivedAccount {
|
||||
val derivationIndex = DerivationIndex(value = 1000).getOrNull()!!
|
||||
private suspend fun getAccountsResponseSync(userWalletId: UserWalletId): GetWalletAccountsResponse? {
|
||||
val store = getAccountsResponseStore(userWalletId = userWalletId)
|
||||
return store.data.firstOrNull()
|
||||
}
|
||||
|
||||
return ArchivedAccount(
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = derivationIndex,
|
||||
),
|
||||
name = AccountName("Archived Account").getOrNull()!!,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex = derivationIndex,
|
||||
tokensCount = 2,
|
||||
networksCount = 1,
|
||||
)
|
||||
private fun getAccountsResponseStore(userWalletId: UserWalletId): AccountsResponseStore {
|
||||
return accountsResponseStoreFactory.create(userWalletId = userWalletId)
|
||||
}
|
||||
|
||||
private fun getArchivedAccountsStore(userWalletId: UserWalletId): ArchivedAccountsStore {
|
||||
return archivedAccountsStoreFactory.create(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
typealias AccountsResponseStore = DataStore<GetWalletAccountsResponse?>
|
||||
|
||||
/**
|
||||
* Factory class for creating and managing instances of [AccountsResponseStore].
|
||||
* This class is responsible for creating a [DataStore] for each unique [UserWalletId].
|
||||
*
|
||||
* @property context application context used to access the file system
|
||||
* @property moshi moshi instance for JSON serialization and deserialization
|
||||
* @property dispatchers coroutine dispatcher provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class AccountsResponseStoreFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private val adapter by lazy { moshi.adapter<GetWalletAccountsResponse?>() }
|
||||
|
||||
private val createdDataStores = ConcurrentHashMap<UserWalletId, AccountsResponseStore>()
|
||||
|
||||
/**
|
||||
* Creates or retrieves an [AccountsResponseStore] for the given [UserWalletId].
|
||||
*
|
||||
* @param userWalletId the unique identifier of the user's wallet
|
||||
*/
|
||||
fun create(userWalletId: UserWalletId): AccountsResponseStore {
|
||||
return createdDataStores.computeIfAbsent(userWalletId) {
|
||||
DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(defaultValue = null, adapter = adapter),
|
||||
produceFile = { context.dataStoreFile(fileName = "wallet_accounts_${userWalletId.stringValue}") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun getAllStores(): Map<UserWalletId, AccountsResponseStore> = createdDataStores.toMap()
|
||||
|
||||
@VisibleForTesting
|
||||
fun clearStores() {
|
||||
createdDataStores.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Store for managing archived accounts with support for data expiration
|
||||
*
|
||||
* @property runtimeStore the underlying runtime shared store for storing the list of archived accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ArchivedAccountsStore(
|
||||
private val runtimeStore: RuntimeStateStore<List<ArchivedAccount>?>,
|
||||
) {
|
||||
|
||||
private var timestamp: Long? = null
|
||||
|
||||
/** Retrieves a flow of archived accounts, filtering out null values */
|
||||
fun get(): Flow<List<ArchivedAccount>> {
|
||||
return runtimeStore.get()
|
||||
.map {
|
||||
if (isDataExpired()) null else it
|
||||
}
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
/** Retrieves the list of archived accounts synchronously, or null if the data is expired */
|
||||
suspend fun getSyncOrNull(): List<ArchivedAccount>? {
|
||||
if (isDataExpired()) return null
|
||||
|
||||
return runtimeStore.getSyncOrNull()
|
||||
}
|
||||
|
||||
/** Stores the provided list of archived accounts [value] */
|
||||
suspend fun store(value: List<ArchivedAccount>) {
|
||||
timestamp = System.currentTimeMillis()
|
||||
|
||||
runtimeStore.store(value)
|
||||
}
|
||||
|
||||
private fun isDataExpired(): Boolean {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val storedTime = timestamp ?: return true
|
||||
|
||||
return currentTime - storedTime >= EXPIRATION_DURATION_MS
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun setTimestamp(time: Long) {
|
||||
timestamp = time
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun clear() {
|
||||
timestamp = null
|
||||
runtimeStore.clear()
|
||||
}
|
||||
|
||||
private companion object Companion {
|
||||
val EXPIRATION_DURATION_MS = 120.seconds.inWholeMicroseconds
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Factory for creating and managing instances of [ArchivedAccountsStore].
|
||||
|
||||
* and reused for each unique [UserWalletId].
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object ArchivedAccountsStoreFactory {
|
||||
|
||||
private val createdRuntimeStores = ConcurrentHashMap<UserWalletId, ArchivedAccountsStore>()
|
||||
|
||||
/**
|
||||
* Creates or retrieves an existing instance of [ArchivedAccountsStore] for the given [userWalletId].
|
||||
*
|
||||
* @param userWalletId the unique identifier for the user wallet
|
||||
*/
|
||||
fun create(userWalletId: UserWalletId): ArchivedAccountsStore {
|
||||
return createdRuntimeStores.computeIfAbsent(userWalletId) {
|
||||
ArchivedAccountsStore(runtimeStore = RuntimeStateStore(defaultValue = null))
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun getAllStores(): Map<UserWalletId, ArchivedAccountsStore> = createdRuntimeStores.toMap()
|
||||
|
||||
@VisibleForTesting
|
||||
fun clearStores() {
|
||||
createdRuntimeStores.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
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.models.TokensGroupType
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
internal fun createWalletAccountDTO(
|
||||
userWalletId: UserWalletId,
|
||||
accountId: String? = null,
|
||||
accountName: String? = null,
|
||||
icon: String? = null,
|
||||
iconColor: String? = null,
|
||||
derivationIndex: Int? = null,
|
||||
tokens: List<UserTokensResponse.Token>? = emptyList(),
|
||||
): WalletAccountDTO {
|
||||
val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)
|
||||
|
||||
return WalletAccountDTO(
|
||||
id = accountId ?: mainAccount.accountId.value,
|
||||
name = accountName ?: mainAccount.accountName.value,
|
||||
derivationIndex = derivationIndex ?: mainAccount.derivationIndex.value,
|
||||
icon = icon ?: mainAccount.icon.value.name,
|
||||
iconColor = iconColor ?: mainAccount.icon.color.name,
|
||||
tokens = tokens,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun createCryptoPortfolio(userWalletId: UserWalletId): Account.CryptoPortfolio {
|
||||
return Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)
|
||||
}
|
||||
|
||||
internal fun createGetWalletAccountsResponse(
|
||||
userWalletId: UserWalletId,
|
||||
groupType: UserTokensResponse.GroupType = UserTokensResponse.GroupType.NETWORK,
|
||||
sortType: UserTokensResponse.SortType = UserTokensResponse.SortType.BALANCE,
|
||||
accountId: String? = null,
|
||||
accountName: String? = null,
|
||||
icon: String? = null,
|
||||
iconColor: String? = null,
|
||||
derivationIndex: Int? = null,
|
||||
tokens: List<UserTokensResponse.Token>? = emptyList(),
|
||||
): GetWalletAccountsResponse {
|
||||
return GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 0,
|
||||
group = groupType,
|
||||
sort = sortType,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = buildList {
|
||||
createWalletAccountDTO(
|
||||
userWalletId = userWalletId,
|
||||
accountId = accountId,
|
||||
accountName = accountName,
|
||||
icon = icon,
|
||||
iconColor = iconColor,
|
||||
derivationIndex = derivationIndex,
|
||||
tokens = tokens,
|
||||
)
|
||||
.let(::add)
|
||||
},
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun createAccountList(
|
||||
userWallet: UserWallet,
|
||||
sortType: TokensSortType = TokensSortType.BALANCE,
|
||||
groupType: TokensGroupType = TokensGroupType.NETWORK,
|
||||
): AccountList {
|
||||
return AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(createCryptoPortfolio(userWallet.walletId)),
|
||||
totalAccounts = 1,
|
||||
sortType = sortType,
|
||||
groupType = groupType,
|
||||
)
|
||||
.getOrNull()!!
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
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.models.TokensGroupType
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class AccountListConverterTest {
|
||||
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { walletId } returns UserWalletId("011")
|
||||
}
|
||||
private val cryptoPortfolioConverterFactory = mockk<CryptoPortfolioConverter.Factory>()
|
||||
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
|
||||
private val converter = AccountListConverter(userWallet, cryptoPortfolioConverterFactory)
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { cryptoPortfolioConverterFactory.create(userWallet) } returns cryptoPortfolioConverter
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
clearMocks(cryptoPortfolioConverter)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Convert {
|
||||
|
||||
@Test
|
||||
fun `cryptoPortfolioConverter throws exception`() {
|
||||
// Arrange
|
||||
val dto = createGetWalletAccountsResponse(userWallet.walletId)
|
||||
val exception = IllegalStateException("Test exception")
|
||||
|
||||
every { cryptoPortfolioConverter.convert(any()) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { converter.convert(dto) }.exceptionOrNull()!!
|
||||
|
||||
// Asset
|
||||
val expected = exception
|
||||
Truth.assertThat(actual).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.message).isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convert(model: ConvertModel) {
|
||||
// Arrange
|
||||
if (model.expected.isSuccess) {
|
||||
model.value.accounts.forEach { dto ->
|
||||
val account = model.expected.getOrNull()!!.accounts
|
||||
.firstOrNull { it.accountId.value == dto.id } as? Account.CryptoPortfolio
|
||||
|
||||
every { cryptoPortfolioConverter.convert(dto) } returns account!!
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
val actual = runCatching { converter.convert(model.value) }
|
||||
|
||||
// Asset
|
||||
actual
|
||||
.onSuccess {
|
||||
val expected = model.expected.getOrNull()
|
||||
Truth.assertThat(it).isEqualTo(expected)
|
||||
}
|
||||
.onFailure {
|
||||
val expected = model.expected.exceptionOrNull() ?: throw it
|
||||
Truth.assertThat(it).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(it.message).isEqualTo(expected.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels(): List<ConvertModel> {
|
||||
return listOf(
|
||||
ConvertModel(
|
||||
value = createGetWalletAccountsResponse(
|
||||
userWalletId = userWallet.walletId,
|
||||
sortType = UserTokensResponse.SortType.BALANCE,
|
||||
groupType = UserTokensResponse.GroupType.NETWORK,
|
||||
),
|
||||
expected = Result.success(
|
||||
createAccountList(
|
||||
userWallet = userWallet,
|
||||
sortType = TokensSortType.BALANCE,
|
||||
groupType = TokensGroupType.NETWORK,
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = createGetWalletAccountsResponse(
|
||||
userWalletId = userWallet.walletId,
|
||||
sortType = UserTokensResponse.SortType.MANUAL,
|
||||
groupType = UserTokensResponse.GroupType.TOKEN,
|
||||
),
|
||||
expected = Result.success(
|
||||
createAccountList(
|
||||
userWallet = userWallet,
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = createGetWalletAccountsResponse(
|
||||
userWalletId = userWallet.walletId,
|
||||
sortType = UserTokensResponse.SortType.MARKETCAP,
|
||||
groupType = UserTokensResponse.GroupType.NONE,
|
||||
),
|
||||
expected = Result.success(
|
||||
createAccountList(
|
||||
userWallet = userWallet,
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 0,
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
),
|
||||
expected = Result.failure(
|
||||
IllegalStateException(
|
||||
"Failed to convert GetWalletAccountsResponse to AccountList: EmptyAccountsList: " +
|
||||
"The accounts list cannot be empty",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConvertModel(
|
||||
val value: GetWalletAccountsResponse,
|
||||
val expected: Result<AccountList>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
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.UserWalletId
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class ArchivedAccountConverterTest {
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val converter = ArchivedAccountConverter(userWalletId = userWalletId)
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convert(model: TestModel) {
|
||||
// Act
|
||||
val actual = runCatching { converter.convert(value = model.value) }
|
||||
|
||||
// Assert
|
||||
actual
|
||||
.onSuccess {
|
||||
val expected = model.expected.getOrNull()!!
|
||||
Truth.assertThat(it).isEqualTo(expected)
|
||||
}
|
||||
.onFailure {
|
||||
val expected = model.expected.exceptionOrNull()!!
|
||||
Truth.assertThat(it).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(it.message).isEqualTo(expected.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels(): List<TestModel> {
|
||||
return listOf(
|
||||
TestModel(
|
||||
value = createDTO(),
|
||||
expected = Result.success(createDomain()),
|
||||
),
|
||||
TestModel(
|
||||
value = createDTO(accountId = "123"),
|
||||
expected = Result.failure(
|
||||
IllegalStateException(
|
||||
"Unable to create AccountId from value: 123. Cause: ${AccountId.Error.InvalidFormat}",
|
||||
),
|
||||
),
|
||||
),
|
||||
TestModel(
|
||||
value = createDTO(name = ""),
|
||||
expected = Result.failure(
|
||||
IllegalStateException(
|
||||
"Unable to create AccountName from value: . Cause: ${AccountName.Error.Empty}",
|
||||
),
|
||||
),
|
||||
),
|
||||
TestModel(
|
||||
value = createDTO(icon = "INVALID_ICON"),
|
||||
expected = Result.failure(
|
||||
IllegalArgumentException(
|
||||
"No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON",
|
||||
),
|
||||
),
|
||||
),
|
||||
TestModel(
|
||||
value = createDTO(iconColor = "INVALID_COLOR"),
|
||||
expected = Result.failure(
|
||||
IllegalArgumentException(
|
||||
"No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR",
|
||||
),
|
||||
),
|
||||
),
|
||||
TestModel(
|
||||
value = createDTO(derivationIndex = -1),
|
||||
expected = Result.failure(
|
||||
IllegalStateException(
|
||||
"Unable to create DerivationIndex from value: -1. " +
|
||||
"Cause: NegativeDerivationIndex: Derivation index cannot be negative: -1",
|
||||
),
|
||||
),
|
||||
),
|
||||
TestModel(
|
||||
value = createDTO(totalTokens = null),
|
||||
expected = Result.failure(
|
||||
IllegalStateException("Total tokens should not be null"),
|
||||
),
|
||||
),
|
||||
TestModel(
|
||||
value = createDTO(totalNetworks = null),
|
||||
expected = Result.failure(
|
||||
IllegalStateException("Total networks should not be null"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDTO(
|
||||
accountId: String = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027",
|
||||
name: String = "Test Account",
|
||||
icon: String = "Letter",
|
||||
iconColor: String = "Azure",
|
||||
derivationIndex: Int = 0,
|
||||
totalTokens: Int? = 1,
|
||||
totalNetworks: Int? = 1,
|
||||
): WalletAccountDTO {
|
||||
return WalletAccountDTO(
|
||||
id = accountId,
|
||||
name = name,
|
||||
derivationIndex = derivationIndex,
|
||||
icon = icon,
|
||||
iconColor = iconColor,
|
||||
tokens = null,
|
||||
totalTokens = totalTokens,
|
||||
totalNetworks = totalNetworks,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDomain(): ArchivedAccount {
|
||||
return ArchivedAccount(
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex(0).getOrNull()!!),
|
||||
name = "Test Account".toAccountName(),
|
||||
derivationIndex = 0.toDerivationIndex(),
|
||||
icon = CryptoPortfolioIcon.ofCustomAccount(
|
||||
value = CryptoPortfolioIcon.Icon.Letter,
|
||||
color = CryptoPortfolioIcon.Color.Azure,
|
||||
),
|
||||
tokensCount = 1,
|
||||
networksCount = 1,
|
||||
)
|
||||
}
|
||||
|
||||
data class TestModel(
|
||||
val value: WalletAccountDTO,
|
||||
val expected: Result<ArchivedAccount>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class CryptoPortfolioConverterTest {
|
||||
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { walletId } returns UserWalletId("011")
|
||||
}
|
||||
|
||||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory = mockk()
|
||||
private val userTokensResponseFactory: UserTokensResponseFactory = mockk()
|
||||
private val converter = CryptoPortfolioConverter(
|
||||
userWallet = userWallet,
|
||||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
userTokensResponseFactory = userTokensResponseFactory,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
clearMocks(responseCryptoCurrenciesFactory, userTokensResponseFactory)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Convert {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convert(model: ConvertModel) {
|
||||
// Act
|
||||
val actual = runCatching { converter.convert(model.value) }
|
||||
|
||||
// Asset
|
||||
actual
|
||||
.onSuccess {
|
||||
val expected = model.expected.getOrNull()
|
||||
Truth.assertThat(it).isEqualTo(expected)
|
||||
}
|
||||
.onFailure {
|
||||
val expected = model.expected.exceptionOrNull() ?: throw it
|
||||
Truth.assertThat(it).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(it.message).isEqualTo(expected.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels(): List<ConvertModel> {
|
||||
return listOf(
|
||||
ConvertModel(
|
||||
value = createWalletAccountDTO(userWalletId = userWallet.walletId),
|
||||
expected = Result.success(createCryptoPortfolio(userWalletId = userWallet.walletId)),
|
||||
),
|
||||
ConvertModel(
|
||||
value = createWalletAccountDTO(userWalletId = userWallet.walletId, accountId = "123"),
|
||||
expected = Result.failure(
|
||||
IllegalStateException(
|
||||
"Unable to create AccountId from value: 123. Cause: ${AccountId.Error.InvalidFormat}",
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = createWalletAccountDTO(userWalletId = userWallet.walletId, accountName = ""),
|
||||
expected = Result.failure(
|
||||
IllegalStateException(
|
||||
"Unable to create AccountName from value: . Cause: ${AccountName.Error.Empty}",
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = createWalletAccountDTO(userWalletId = userWallet.walletId, icon = "INVALID_ICON"),
|
||||
expected = Result.failure(
|
||||
IllegalArgumentException(
|
||||
"No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON",
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = createWalletAccountDTO(userWalletId = userWallet.walletId, iconColor = "INVALID_COLOR"),
|
||||
expected = Result.failure(
|
||||
IllegalArgumentException(
|
||||
"No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR",
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = createWalletAccountDTO(userWalletId = userWallet.walletId, derivationIndex = -1),
|
||||
expected = Result.failure(
|
||||
IllegalStateException(
|
||||
"Unable to create DerivationIndex from value: -1. " +
|
||||
"Cause: NegativeDerivationIndex: Derivation index cannot be negative: -1",
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = createWalletAccountDTO(userWalletId = userWallet.walletId, tokens = null),
|
||||
expected = Result.failure(
|
||||
IllegalStateException("Tokens should not be null"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConvertModel(
|
||||
val value: WalletAccountDTO,
|
||||
val expected: Result<Account.CryptoPortfolio>,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ConvertBack {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convertBack(model: ConvertBackModel) {
|
||||
// Act
|
||||
val actual = converter.convertBack(model.value)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels(): List<ConvertBackModel> {
|
||||
return listOf(
|
||||
ConvertBackModel(
|
||||
value = createCryptoPortfolio(userWalletId = userWallet.walletId),
|
||||
expected = createWalletAccountDTO(userWalletId = userWallet.walletId),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConvertBackModel(
|
||||
val value: Account.CryptoPortfolio,
|
||||
val expected: WalletAccountDTO,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.data.account.converter.CryptoPortfolioIconConverter.DataModel
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class CryptoPortfolioIconConverterTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convert(model: TestModel) {
|
||||
// Act
|
||||
val actual = runCatching { CryptoPortfolioIconConverter.convert(model.value) }
|
||||
|
||||
// Assert
|
||||
actual
|
||||
.onSuccess {
|
||||
val expected = model.expected.getOrNull()!!
|
||||
Truth.assertThat(it).isEqualTo(expected)
|
||||
}
|
||||
.onFailure {
|
||||
val expected = model.expected.exceptionOrNull()!!
|
||||
Truth.assertThat(it).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(it.message).isEqualTo(expected.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels(): List<TestModel> {
|
||||
return listOf(
|
||||
TestModel(
|
||||
value = DataModel(icon = "Letter", color = "Azure"),
|
||||
expected = Result.success(
|
||||
CryptoPortfolioIcon.ofCustomAccount(
|
||||
value = CryptoPortfolioIcon.Icon.Letter,
|
||||
color = CryptoPortfolioIcon.Color.Azure,
|
||||
),
|
||||
),
|
||||
),
|
||||
TestModel(
|
||||
value = DataModel(icon = "INVALID_ICON", color = "Azure"),
|
||||
expected = Result.failure(
|
||||
IllegalArgumentException(
|
||||
"No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON",
|
||||
),
|
||||
),
|
||||
),
|
||||
TestModel(
|
||||
value = DataModel(icon = "Letter", color = "INVALID_COLOR"),
|
||||
expected = Result.failure(
|
||||
IllegalArgumentException(
|
||||
"No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class TestModel(
|
||||
val value: DataModel,
|
||||
val expected: Result<CryptoPortfolioIcon>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
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.models.TokensGroupType
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class GetWalletAccountsResponseConverterTest {
|
||||
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { walletId } returns UserWalletId("011")
|
||||
}
|
||||
private val cryptoPortfolioConverterFactory = mockk<CryptoPortfolioConverter.Factory>()
|
||||
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
|
||||
private val converter = GetWalletAccountsResponseConverter(
|
||||
userWallet = userWallet,
|
||||
version = 0,
|
||||
cryptoPortfolioConverterFactory = cryptoPortfolioConverterFactory,
|
||||
)
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { cryptoPortfolioConverterFactory.create(userWallet) } returns cryptoPortfolioConverter
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
clearMocks(cryptoPortfolioConverter)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Convert {
|
||||
|
||||
@Test
|
||||
fun `cryptoPortfolioConverter throws exception`() {
|
||||
// Arrange
|
||||
val domain = createAccountList(userWallet = userWallet)
|
||||
val exception = IllegalStateException("Test exception")
|
||||
|
||||
every { cryptoPortfolioConverter.convertBack(any()) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { converter.convert(domain) }.exceptionOrNull()!!
|
||||
|
||||
// Asset
|
||||
val expected = exception
|
||||
Truth.assertThat(actual).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.message).isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convert(model: ConvertModel) {
|
||||
// Arrange
|
||||
if (model.expected.isSuccess) {
|
||||
model.value.accounts.forEach { domain ->
|
||||
val dto = model.expected.getOrNull()!!.accounts.firstOrNull { it.id == domain.accountId.value }
|
||||
|
||||
every { cryptoPortfolioConverter.convertBack(domain as Account.CryptoPortfolio) } returns dto!!
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
val actual = runCatching { converter.convert(model.value) }
|
||||
|
||||
// Asset
|
||||
actual
|
||||
.onSuccess {
|
||||
val expected = model.expected.getOrNull()
|
||||
Truth.assertThat(it).isEqualTo(expected)
|
||||
}
|
||||
.onFailure {
|
||||
val expected = model.expected.exceptionOrNull() ?: throw it
|
||||
Truth.assertThat(it).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(it.message).isEqualTo(expected.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels(): List<ConvertModel> {
|
||||
return listOf(
|
||||
ConvertModel(
|
||||
value = createAccountList(
|
||||
userWallet = userWallet,
|
||||
sortType = TokensSortType.BALANCE,
|
||||
groupType = TokensGroupType.NETWORK,
|
||||
),
|
||||
expected = Result.success(
|
||||
createGetWalletAccountsResponse(
|
||||
userWalletId = userWallet.walletId,
|
||||
sortType = UserTokensResponse.SortType.BALANCE,
|
||||
groupType = UserTokensResponse.GroupType.NETWORK,
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = createAccountList(
|
||||
userWallet = userWallet,
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
expected = Result.success(
|
||||
createGetWalletAccountsResponse(
|
||||
userWalletId = userWallet.walletId,
|
||||
sortType = UserTokensResponse.SortType.MANUAL,
|
||||
groupType = UserTokensResponse.GroupType.NONE,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConvertModel(
|
||||
val value: AccountList,
|
||||
val expected: Result<GetWalletAccountsResponse>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
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.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
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class SaveWalletAccountsResponseConverterTest {
|
||||
|
||||
@Test
|
||||
fun convert() {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns UserWalletId("011")
|
||||
}
|
||||
|
||||
val accountList = AccountList(
|
||||
userWallet = userWallet,
|
||||
accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)),
|
||||
totalAccounts = 1,
|
||||
)
|
||||
.getOrNull()!!
|
||||
|
||||
// Act
|
||||
val actual = SaveWalletAccountsResponseConverter.convert(value = accountList)
|
||||
|
||||
// Assert
|
||||
val expected = SaveWalletAccountsResponse(
|
||||
accounts = listOf(
|
||||
WalletAccountDTO(
|
||||
id = accountList.mainAccount.accountId.value,
|
||||
name = accountList.mainAccount.accountName.value,
|
||||
derivationIndex = accountList.mainAccount.derivationIndex.value,
|
||||
icon = accountList.mainAccount.icon.value.name,
|
||||
iconColor = accountList.mainAccount.icon.color.name,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.TokensGroupType
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class TokensGroupTypeConverterTest {
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Convert {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convert(model: ConvertModel) {
|
||||
// Act
|
||||
val actual = TokensGroupTypeConverter.convert(model.value)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels(): List<ConvertModel> {
|
||||
return listOf(
|
||||
ConvertModel(
|
||||
value = UserTokensResponse.GroupType.NETWORK,
|
||||
expected = TokensGroupType.NETWORK,
|
||||
),
|
||||
ConvertModel(
|
||||
value = UserTokensResponse.GroupType.NONE,
|
||||
expected = TokensGroupType.NONE,
|
||||
),
|
||||
ConvertModel(
|
||||
value = UserTokensResponse.GroupType.TOKEN,
|
||||
expected = TokensGroupType.NONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConvertModel(
|
||||
val value: UserTokensResponse.GroupType,
|
||||
val expected: TokensGroupType,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ConvertBack {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convertBack(model: ConvertBackModel) {
|
||||
// Act
|
||||
val actual = TokensGroupTypeConverter.convertBack(model.value)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels(): List<ConvertBackModel> {
|
||||
return listOf(
|
||||
ConvertBackModel(
|
||||
value = TokensGroupType.NETWORK,
|
||||
expected = UserTokensResponse.GroupType.NETWORK,
|
||||
),
|
||||
ConvertBackModel(
|
||||
value = TokensGroupType.NONE,
|
||||
expected = UserTokensResponse.GroupType.NONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConvertBackModel(
|
||||
val value: TokensGroupType,
|
||||
val expected: UserTokensResponse.GroupType,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class TokensSortTypeConverterTest {
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Convert {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convert(model: ConvertModel) {
|
||||
// Act
|
||||
val actual = TokensSortTypeConverter.convert(model.value)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
fun provideTestModels() = listOf(
|
||||
ConvertModel(
|
||||
value = UserTokensResponse.SortType.BALANCE,
|
||||
expected = TokensSortType.BALANCE,
|
||||
),
|
||||
ConvertModel(
|
||||
value = UserTokensResponse.SortType.MANUAL,
|
||||
expected = TokensSortType.NONE,
|
||||
),
|
||||
ConvertModel(
|
||||
value = UserTokensResponse.SortType.MARKETCAP,
|
||||
expected = TokensSortType.NONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class ConvertModel(
|
||||
val value: UserTokensResponse.SortType,
|
||||
val expected: TokensSortType,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ConvertBack {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun convertBack(model: ConvertBackModel) {
|
||||
// Act
|
||||
val actual = TokensSortTypeConverter.convertBack(model.value)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
fun provideTestModels() = listOf(
|
||||
ConvertBackModel(
|
||||
value = TokensSortType.BALANCE,
|
||||
expected = UserTokensResponse.SortType.BALANCE,
|
||||
),
|
||||
ConvertBackModel(
|
||||
value = TokensSortType.NONE,
|
||||
expected = UserTokensResponse.SortType.MANUAL,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class ConvertBackModel(
|
||||
val value: TokensSortType,
|
||||
val expected: UserTokensResponse.SortType,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,664 @@
|
|||
package com.tangem.data.account.repository
|
||||
|
||||
import arrow.core.None
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.data.account.converter.*
|
||||
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.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.models.account.Account.CryptoPortfolio
|
||||
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.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultAccountsCRUDRepositoryTest {
|
||||
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
|
||||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk()
|
||||
private val accountsResponseStore: AccountsResponseStore = mockk()
|
||||
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
|
||||
|
||||
private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory = mockk()
|
||||
private val archivedAccountsInnerStore = RuntimeStateStore<List<ArchivedAccount>?>(defaultValue = null)
|
||||
private val archivedAccountsStore = ArchivedAccountsStore(runtimeStore = archivedAccountsInnerStore)
|
||||
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
|
||||
private val convertersContainer: AccountConverterFactoryContainer = mockk()
|
||||
private val accountListConverter: AccountListConverter = mockk()
|
||||
private val cryptoPortfolioConverter: CryptoPortfolioConverter = mockk()
|
||||
|
||||
private val repository = DefaultAccountsCRUDRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
archivedAccountsStoreFactory = archivedAccountsStoreFactory,
|
||||
userWalletsStore = userWalletsStore,
|
||||
convertersContainer = convertersContainer,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore
|
||||
every { accountsResponseStore.data } returns accountsResponseStoreFlow
|
||||
|
||||
every { convertersContainer.createAccountListConverter(userWalletId) } returns accountListConverter
|
||||
every { convertersContainer.createCryptoPortfolioConverter(userWalletId) } returns cryptoPortfolioConverter
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
every { archivedAccountsStoreFactory.create(userWalletId) } returns archivedAccountsStore
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
accountsResponseStoreFlow.value = null
|
||||
archivedAccountsInnerStore.clear()
|
||||
|
||||
clearMocks(
|
||||
tangemTechApi,
|
||||
archivedAccountsStoreFactory,
|
||||
accountListConverter,
|
||||
cryptoPortfolioConverter,
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetAccountListSync {
|
||||
|
||||
@Test
|
||||
fun `getAccounts should return None when account list response is null`() = runTest {
|
||||
// Arrange
|
||||
accountsResponseStoreFlow.value = null
|
||||
|
||||
// Act
|
||||
val actual = repository.getAccountListSync(userWalletId)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(None)
|
||||
|
||||
verifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
verify(inverse = true) { accountListConverter.convert(value = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAccounts should return AccountList when account list response is not null`() = runTest {
|
||||
// Arrange
|
||||
val response = mockk<GetWalletAccountsResponse>()
|
||||
val accountList = mockk<AccountList>()
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
every { accountListConverter.convert(response) } returns accountList
|
||||
|
||||
// Act
|
||||
val actual = repository.getAccountListSync(userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = accountList.toOption()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
verifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
convertersContainer.createAccountListConverter(userWalletId = userWalletId)
|
||||
accountListConverter.convert(response)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAccounts should throw exception if converter throws exception`() = runTest {
|
||||
// Arrange
|
||||
val response = mockk<GetWalletAccountsResponse>()
|
||||
mockk<AccountList>()
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
val exception = Exception("Test error")
|
||||
|
||||
every { accountListConverter.convert(response) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { repository.getAccountListSync(userWalletId) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
val expected = exception
|
||||
Truth.assertThat(actual).isSameInstanceAs(expected)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message)
|
||||
|
||||
verifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
convertersContainer.createAccountListConverter(userWalletId = userWalletId)
|
||||
accountListConverter.convert(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetAccountSync {
|
||||
|
||||
private val accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main)
|
||||
|
||||
@Test
|
||||
fun `getAccount should return None when account response is null`() = runTest {
|
||||
// Arrange
|
||||
val response = null
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
// Act
|
||||
val actual = repository.getAccountSync(accountId)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(None)
|
||||
|
||||
verifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
verify(inverse = true) { convertersContainer.createCryptoPortfolioConverter(userWalletId = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAccount should return None when accountDto is not found`() = runTest {
|
||||
// Arrange
|
||||
val response = mockk<GetWalletAccountsResponse> {
|
||||
every { this@mockk.accounts } returns emptyList()
|
||||
}
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
// Act
|
||||
val actual = repository.getAccountSync(accountId)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(None)
|
||||
|
||||
verifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
}
|
||||
|
||||
verify(inverse = true) { convertersContainer.createCryptoPortfolioConverter(userWalletId = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAccount should return Account_CryptoPortfolio when account response is not null`() = runTest {
|
||||
// Arrange
|
||||
val accountDTO = mockk<WalletAccountDTO> {
|
||||
every { this@mockk.id } returns accountId.value
|
||||
}
|
||||
|
||||
val response = mockk<GetWalletAccountsResponse> {
|
||||
every { this@mockk.accounts } returns listOf(accountDTO)
|
||||
}
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
val cryptoPortfolio = mockk<CryptoPortfolio>()
|
||||
|
||||
every { cryptoPortfolioConverter.convert(accountDTO) } returns cryptoPortfolio
|
||||
|
||||
// Act
|
||||
val actual = repository.getAccountSync(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = cryptoPortfolio.toOption()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
verifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
convertersContainer.createCryptoPortfolioConverter(userWalletId)
|
||||
cryptoPortfolioConverter.convert(accountDTO)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAccount should throw exception if converter throws exception`() = runTest {
|
||||
// Arrange
|
||||
val accountDTO = mockk<WalletAccountDTO> {
|
||||
every { this@mockk.id } returns accountId.value
|
||||
}
|
||||
|
||||
val response = mockk<GetWalletAccountsResponse> {
|
||||
every { this@mockk.accounts } returns listOf(accountDTO)
|
||||
}
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
val exception = Exception("Test error")
|
||||
|
||||
every { cryptoPortfolioConverter.convert(accountDTO) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { repository.getAccountSync(accountId) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
val expected = exception
|
||||
Truth.assertThat(actual).isSameInstanceAs(expected)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message)
|
||||
|
||||
verifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
convertersContainer.createCryptoPortfolioConverter(userWalletId)
|
||||
cryptoPortfolioConverter.convert(accountDTO)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetArchivedAccountSync {
|
||||
|
||||
private val accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main)
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccount should return None when archived accounts are null`() = runTest {
|
||||
// Arrange
|
||||
archivedAccountsInnerStore.store(value = null)
|
||||
|
||||
// Act
|
||||
val actual = repository.getArchivedAccountSync(accountId)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(None)
|
||||
|
||||
coVerifyOrder {
|
||||
archivedAccountsStoreFactory.create(userWalletId)
|
||||
archivedAccountsStore.getSyncOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccount should return None when archived account not found`() = runTest {
|
||||
// Arrange
|
||||
archivedAccountsInnerStore.store(value = listOf())
|
||||
archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() + 3.minutes.inWholeMicroseconds)
|
||||
|
||||
// Act
|
||||
val actual = repository.getArchivedAccountSync(accountId)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(None)
|
||||
|
||||
coVerifyOrder {
|
||||
archivedAccountsStoreFactory.create(userWalletId)
|
||||
archivedAccountsStore.getSyncOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccount should return ArchivedAccount when found`() = runTest {
|
||||
// Arrange
|
||||
val archivedAccount = ArchivedAccount(
|
||||
accountId = accountId,
|
||||
name = AccountName("Archived Account").getOrNull()!!,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
tokensCount = 0,
|
||||
networksCount = 0,
|
||||
)
|
||||
|
||||
archivedAccountsInnerStore.store(value = listOf(archivedAccount))
|
||||
archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() + 3.minutes.inWholeMicroseconds)
|
||||
|
||||
// Act
|
||||
val actual = repository.getArchivedAccountSync(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = archivedAccount.toOption()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
archivedAccountsStoreFactory.create(userWalletId)
|
||||
archivedAccountsStore.getSyncOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccount should throws exception when store throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test error")
|
||||
|
||||
coEvery { archivedAccountsStoreFactory.create(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { repository.getArchivedAccountSync(accountId) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
val expected = exception
|
||||
Truth.assertThat(actual).isSameInstanceAs(expected)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message)
|
||||
|
||||
coVerifyOrder { archivedAccountsStoreFactory.create(userWalletId) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetArchivedAccountListSync {
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccountListSync should return None when archived accounts are null`() = runTest {
|
||||
// Arrange
|
||||
archivedAccountsInnerStore.store(value = null)
|
||||
|
||||
// Act
|
||||
val actual = repository.getArchivedAccountListSync(userWalletId)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(None)
|
||||
|
||||
coVerifyOrder {
|
||||
archivedAccountsStoreFactory.create(userWalletId)
|
||||
archivedAccountsStore.getSyncOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccountListSync should return Option with list when archived accounts exist`() = runTest {
|
||||
// Arrange
|
||||
val archivedAccount1 = mockk<ArchivedAccount>()
|
||||
val archivedAccount2 = mockk<ArchivedAccount>()
|
||||
val archivedAccounts = listOf(archivedAccount1, archivedAccount2)
|
||||
archivedAccountsInnerStore.store(value = archivedAccounts)
|
||||
archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() + 3.minutes.inWholeMicroseconds)
|
||||
|
||||
// Act
|
||||
val actual = repository.getArchivedAccountListSync(userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = archivedAccounts.toOption()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
archivedAccountsStoreFactory.create(userWalletId)
|
||||
archivedAccountsStore.getSyncOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccountListSync should throw exception when store throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test error")
|
||||
coEvery { archivedAccountsStoreFactory.create(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { repository.getArchivedAccountListSync(userWalletId) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isSameInstanceAs(exception)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { archivedAccountsStoreFactory.create(userWalletId) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetArchivedAccounts {
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccounts should emit empty list when no archived accounts`() = runTest {
|
||||
// Arrange
|
||||
archivedAccountsInnerStore.store(value = null)
|
||||
|
||||
val archivedAccountsFlow = repository.getArchivedAccounts(userWalletId)
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(archivedAccountsFlow)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
|
||||
coVerifyOrder {
|
||||
archivedAccountsStoreFactory.create(userWalletId)
|
||||
archivedAccountsStore.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccounts should emit list of archived accounts when present`() = runTest {
|
||||
// Arrange
|
||||
val archivedAccount1 = mockk<ArchivedAccount>()
|
||||
val archivedAccount2 = mockk<ArchivedAccount>()
|
||||
val archivedAccounts = listOf(archivedAccount1, archivedAccount2)
|
||||
|
||||
archivedAccountsInnerStore.store(value = archivedAccounts)
|
||||
archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() + 3.minutes.inWholeMicroseconds)
|
||||
|
||||
val archivedAccountsFlow = repository.getArchivedAccounts(userWalletId)
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(archivedAccountsFlow)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(archivedAccounts)
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
coVerifyOrder {
|
||||
archivedAccountsStoreFactory.create(userWalletId)
|
||||
archivedAccountsStore.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getArchivedAccounts should throw exception when store throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test error")
|
||||
coEvery { archivedAccountsStoreFactory.create(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { repository.getArchivedAccounts(userWalletId) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isSameInstanceAs(exception)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { archivedAccountsStoreFactory.create(userWalletId) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class FetchArchivedAccounts {
|
||||
|
||||
private val accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex.Main)
|
||||
|
||||
@Test
|
||||
fun `fetchArchivedAccounts should store archived accounts in store`() = runTest {
|
||||
// Arrange
|
||||
val accountDTO = WalletAccountDTO(
|
||||
id = accountId.value,
|
||||
name = "Archived Account",
|
||||
derivationIndex = 0,
|
||||
icon = CryptoPortfolioIcon.Icon.Wallet.name,
|
||||
iconColor = CryptoPortfolioIcon.Color.DullLavender.name,
|
||||
totalNetworks = 0,
|
||||
totalTokens = 0,
|
||||
)
|
||||
|
||||
val apiResponse = mockk<GetWalletArchivedAccountsResponse> {
|
||||
every { this@mockk.accounts } returns listOf(accountDTO)
|
||||
}
|
||||
|
||||
val archivedAccount = ArchivedAccountConverter(userWalletId).convert(accountDTO)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue)
|
||||
} returns ApiResponse.Success(apiResponse)
|
||||
|
||||
// Act
|
||||
repository.fetchArchivedAccounts(userWalletId)
|
||||
val actual = archivedAccountsStore.getSyncOrNull()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly(archivedAccount)
|
||||
|
||||
coVerifyOrder {
|
||||
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue)
|
||||
archivedAccountsStoreFactory.create(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchArchivedAccounts should throw exception if API returns error`() = runTest { // Arrange
|
||||
val exception = Exception("API error")
|
||||
coEvery { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { repository.fetchArchivedAccounts(userWalletId) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isSameInstanceAs(exception)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
Truth.assertThat(archivedAccountsStore.getSyncOrNull()).isNull()
|
||||
|
||||
coVerify { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class SaveAccounts {
|
||||
|
||||
private val version = 1
|
||||
|
||||
@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(userWallet = userWallet)
|
||||
|
||||
val accountsResponse = mockk<GetWalletAccountsResponse> {
|
||||
every { this@mockk.wallet.version } returns version
|
||||
}
|
||||
|
||||
accountsResponseStoreFlow.value = accountsResponse
|
||||
|
||||
val body = SaveWalletAccountsResponseConverter.convert(value = accountList)
|
||||
|
||||
val apiResponse = ApiResponse.Success(Unit)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
ifMatch = version.toString(),
|
||||
body = body,
|
||||
)
|
||||
} returns apiResponse
|
||||
|
||||
val converter = mockk<GetWalletAccountsResponseConverter> {
|
||||
every { this@mockk.convert(accountList) } returns accountsResponse
|
||||
}
|
||||
|
||||
every {
|
||||
convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet, version = version)
|
||||
} returns converter
|
||||
|
||||
coEvery { accountsResponseStore.updateData(transform = any()) } returns accountsResponse
|
||||
|
||||
// Act
|
||||
repository.saveAccounts(accountList)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(accountsResponseStoreFlow.value).isEqualTo(accountsResponse)
|
||||
|
||||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
tangemTechApi.saveWalletAccounts(userWalletId.stringValue, version.toString(), body)
|
||||
convertersContainer.getWalletAccountsResponseCF.create(userWallet, version)
|
||||
converter.convert(accountList)
|
||||
accountsResponseStore.updateData(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveAccounts if API request is failed`() = runTest {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
val accountList = AccountList.empty(userWallet = userWallet)
|
||||
|
||||
val accountsResponse = mockk<GetWalletAccountsResponse> {
|
||||
every { this@mockk.wallet.version } returns version
|
||||
}
|
||||
|
||||
accountsResponseStoreFlow.value = accountsResponse
|
||||
|
||||
val body = SaveWalletAccountsResponseConverter.convert(value = accountList)
|
||||
|
||||
val apiResponse = ApiResponse.Error(cause = ApiResponseError.NetworkException) as ApiResponse<Unit>
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
ifMatch = version.toString(),
|
||||
body = body,
|
||||
)
|
||||
} returns apiResponse
|
||||
|
||||
// Act
|
||||
val actual = runCatching { repository.saveAccounts(accountList) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(ApiResponseError.NetworkException)
|
||||
|
||||
coVerifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
tangemTechApi.saveWalletAccounts(userWalletId.stringValue, version.toString(), body)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
convertersContainer.getWalletAccountsResponseCF.create(any(), any())
|
||||
accountsResponseStore.updateData(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import android.content.Context
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class AccountsResponseStoreFactoryTest {
|
||||
|
||||
private val context: Context = mockk()
|
||||
private val moshi: Moshi = Moshi.Builder().build()
|
||||
private val factory: AccountsResponseStoreFactory = AccountsResponseStoreFactory(
|
||||
context = context,
|
||||
moshi = moshi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
fun setup() {
|
||||
clearMocks(context)
|
||||
factory.clearStores()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates new data store for unique userWalletId`() {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId("011")
|
||||
val createdStore = factory.create(userWalletId = userWalletId)
|
||||
|
||||
// Actual
|
||||
val actual = factory.getAllStores()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly(userWalletId, createdStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reuses existing data store for same userWalletId`() {
|
||||
val userWalletId = UserWalletId("011")
|
||||
|
||||
// Arrange (first creation)
|
||||
val firstStore = factory.create(userWalletId = userWalletId)
|
||||
|
||||
// Act (first creation)
|
||||
val actual1 = factory.getAllStores()
|
||||
|
||||
// Assert (first creation)
|
||||
Truth.assertThat(actual1).containsExactly(userWalletId, firstStore)
|
||||
|
||||
// Arrange (second creation)
|
||||
val secondStore = factory.create(userWalletId = userWalletId)
|
||||
|
||||
// Act (second creation)
|
||||
val actual2 = factory.getAllStores()
|
||||
|
||||
// Assert (second creation)
|
||||
Truth.assertThat(actual2).containsExactly(userWalletId, secondStore)
|
||||
Truth.assertThat(firstStore).isSameInstanceAs(secondStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates separate data stores for different userWalletIds`() {
|
||||
// Arrange (first creation)
|
||||
val firstWalletId = UserWalletId("011")
|
||||
val firstStore = factory.create(userWalletId = firstWalletId)
|
||||
|
||||
// Act (first creation)
|
||||
val actual1 = factory.getAllStores()
|
||||
|
||||
// Assert (first creation)
|
||||
Truth.assertThat(actual1).containsExactly(firstWalletId, firstStore)
|
||||
|
||||
// Arrange (second creation)
|
||||
val secondWalletId = UserWalletId("011")
|
||||
val secondStore = factory.create(userWalletId = secondWalletId)
|
||||
|
||||
// Act (second creation)
|
||||
val actual2 = factory.getAllStores()
|
||||
|
||||
// Assert (second creation)
|
||||
val expected = mapOf(firstWalletId to firstStore, secondWalletId to secondStore)
|
||||
Truth.assertThat(actual2).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class ArchivedAccountsStoreFactoryTest {
|
||||
|
||||
private val factory = ArchivedAccountsStoreFactory
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
factory.clearStores()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates new store for unique userWalletId`() {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId("001")
|
||||
val createdStore = factory.create(userWalletId)
|
||||
|
||||
// Act
|
||||
val actual = factory.getAllStores()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly(userWalletId, createdStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reuses existing data store for same userWalletId`() {
|
||||
val userWalletId = UserWalletId("011")
|
||||
|
||||
// Arrange (first creation)
|
||||
val firstStore = factory.create(userWalletId = userWalletId)
|
||||
|
||||
// Act (first creation)
|
||||
val actual1 = factory.getAllStores()
|
||||
|
||||
// Assert (first creation)
|
||||
Truth.assertThat(actual1).containsExactly(userWalletId, firstStore)
|
||||
|
||||
// Arrange (second creation)
|
||||
val secondStore = factory.create(userWalletId = userWalletId)
|
||||
|
||||
// Act (second creation)
|
||||
val actual2 = factory.getAllStores()
|
||||
|
||||
// Assert (second creation)
|
||||
Truth.assertThat(actual2).containsExactly(userWalletId, secondStore)
|
||||
Truth.assertThat(firstStore).isSameInstanceAs(secondStore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates separate data stores for different userWalletIds`() {
|
||||
// Arrange (first creation)
|
||||
val firstWalletId = UserWalletId("011")
|
||||
val firstStore = factory.create(userWalletId = firstWalletId)
|
||||
|
||||
// Act (first creation)
|
||||
val actual1 = factory.getAllStores()
|
||||
|
||||
// Assert (first creation)
|
||||
Truth.assertThat(actual1).containsExactly(firstWalletId, firstStore)
|
||||
|
||||
// Arrange (second creation)
|
||||
val secondWalletId = UserWalletId("011")
|
||||
val secondStore = factory.create(userWalletId = secondWalletId)
|
||||
|
||||
// Act (second creation)
|
||||
val actual2 = factory.getAllStores()
|
||||
|
||||
// Assert (second creation)
|
||||
val expected = mapOf(firstWalletId to firstStore, secondWalletId to secondStore)
|
||||
Truth.assertThat(actual2).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.tangem.data.account.store
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.getEmittedValues
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
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.UserWalletId
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class ArchivedAccountsStoreTest {
|
||||
|
||||
private val runtimeStore: RuntimeStateStore<List<ArchivedAccount>?> = RuntimeStateStore(defaultValue = null)
|
||||
private val archivedAccountsStore: ArchivedAccountsStore = ArchivedAccountsStore(runtimeStore = runtimeStore)
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
archivedAccountsStore.clear()
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Get {
|
||||
|
||||
@Test
|
||||
fun `get returns empty flow`() = runTest {
|
||||
// Act
|
||||
val actual = getEmittedValues(archivedAccountsStore.get())
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // nothing emmited
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get returns flow with not expired data`() = runTest {
|
||||
// Arrange
|
||||
val archivedAccount = createArchivedAccount()
|
||||
archivedAccountsStore.store(value = listOf(archivedAccount))
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(archivedAccountsStore.get())
|
||||
|
||||
// Assert
|
||||
val expected = listOf(archivedAccount)
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get returns flow with expired data`() = runTest {
|
||||
// Arrange
|
||||
archivedAccountsStore.store(value = listOf(createArchivedAccount()))
|
||||
archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() - 120.seconds.inWholeMicroseconds)
|
||||
|
||||
// Act
|
||||
val actual = getEmittedValues(archivedAccountsStore.get())
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty() // nothing emmited
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetSyncOrnNull {
|
||||
|
||||
@Test
|
||||
fun `getSyncOrNull returns null`() = runTest {
|
||||
// Act
|
||||
val actual = archivedAccountsStore.getSyncOrNull()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get returns flow with not expired data`() = runTest {
|
||||
// Arrange
|
||||
val archivedAccount = createArchivedAccount()
|
||||
archivedAccountsStore.store(value = listOf(archivedAccount))
|
||||
|
||||
// Act
|
||||
val actual = archivedAccountsStore.getSyncOrNull()
|
||||
|
||||
// Assert
|
||||
val expected = archivedAccount
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get returns flow with expired data`() = runTest {
|
||||
// Arrange
|
||||
archivedAccountsStore.store(value = listOf(createArchivedAccount()))
|
||||
archivedAccountsStore.setTimestamp(time = System.currentTimeMillis() - 120.seconds.inWholeMicroseconds)
|
||||
|
||||
// Act
|
||||
val actual = archivedAccountsStore.getSyncOrNull()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun store() = runTest {
|
||||
// Arrange
|
||||
val archivedAccount = createArchivedAccount()
|
||||
|
||||
// Act
|
||||
archivedAccountsStore.store(value = listOf(archivedAccount))
|
||||
val actual = runtimeStore.getSyncOrNull()
|
||||
|
||||
// Assert
|
||||
val expected = archivedAccount
|
||||
Truth.assertThat(actual).containsExactly(expected)
|
||||
}
|
||||
|
||||
private fun createArchivedAccount(): ArchivedAccount {
|
||||
return ArchivedAccount(
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = UserWalletId("011"),
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
),
|
||||
name = AccountName("Archived Account").getOrNull()!!,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
tokensCount = 2,
|
||||
networksCount = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,9 @@ package com.tangem.data.common.currency
|
|||
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import javax.inject.Inject
|
||||
|
||||
class UserTokensResponseFactory {
|
||||
class UserTokensResponseFactory @Inject constructor() {
|
||||
|
||||
fun createUserTokensResponse(
|
||||
currencies: List<CryptoCurrency>,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import com.tangem.utils.coroutines.runCatching
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.collections.mutableSetOf
|
||||
|
||||
typealias SeedPhraseNotificationsStatuses = Map<UserWalletId, SeedPhraseNotificationsStatus>
|
||||
|
||||
|
|
@ -50,6 +51,9 @@ internal class DefaultWalletsRepository(
|
|||
private val authProvider: AuthProvider,
|
||||
) : WalletsRepository {
|
||||
|
||||
private val upgradeWalletNotificationDisabled: MutableStateFlow<Set<UserWalletId>> =
|
||||
MutableStateFlow(mutableSetOf<UserWalletId>())
|
||||
|
||||
override suspend fun shouldSaveUserWalletsSync(): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
|
||||
}
|
||||
|
|
@ -333,6 +337,16 @@ internal class DefaultWalletsRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow<Boolean> {
|
||||
return upgradeWalletNotificationDisabled.map {
|
||||
it.contains(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) {
|
||||
upgradeWalletNotificationDisabled.update { it.plus(userWalletId) }
|
||||
}
|
||||
|
||||
override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) {
|
||||
tangemTechApi.updateWallet(
|
||||
walletId = walletId,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessor
|
||||
import com.tangem.domain.wallets.usecase.BackendId
|
||||
import com.tangem.hot.sdk.model.DeriveWalletRequest
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.data.wallets.hot
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.copy
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessor
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
|
|
@ -11,22 +11,24 @@ import com.tangem.hot.sdk.exception.WrongPasswordException
|
|||
import com.tangem.hot.sdk.model.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class HotWalletAccessor @Inject constructor(
|
||||
class DefaultHotWalletAccessor @Inject constructor(
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
) {
|
||||
) : HotWalletAccessor {
|
||||
|
||||
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> =
|
||||
override suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> =
|
||||
hotSdkRequest(hotWalletId) { unlock ->
|
||||
tangemHotSdk.signHashes(unlockHotWallet = unlock, dataToSign = dataToSign)
|
||||
}
|
||||
|
||||
suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse =
|
||||
hotSdkRequest(hotWalletId) { unlock ->
|
||||
tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request)
|
||||
}
|
||||
override suspend fun derivePublicKeys(
|
||||
hotWalletId: HotWalletId,
|
||||
request: DeriveWalletRequest,
|
||||
): DerivedPublicKeyResponse = hotSdkRequest(hotWalletId) { unlock ->
|
||||
tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request)
|
||||
}
|
||||
|
||||
private suspend fun <T> hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T {
|
||||
val isAccessCodeRequired = walletsRepository.requireAccessCode()
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessor
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.operations.sign.SignData
|
||||
import dagger.assisted.Assisted
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue