Updated on 2026-08-14
This commit is contained in:
commit
2f62d482ca
635 changed files with 18802 additions and 9118 deletions
|
|
@ -9,9 +9,15 @@ android {
|
|||
namespace = "com.tangem.data.account"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region Project - Core
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.configToggles)
|
||||
api(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
|
|
@ -20,18 +26,43 @@ dependencies {
|
|||
api(projects.domain.models)
|
||||
// endregion
|
||||
|
||||
// Project - Data
|
||||
implementation(projects.core.datasource)
|
||||
// region Project - Data
|
||||
implementation(projects.data.common)
|
||||
// endregion
|
||||
|
||||
// region Project - Libs
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
// endregion
|
||||
|
||||
// region Tangem dependencies
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain)
|
||||
// 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,42 @@
|
|||
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,
|
||||
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(
|
||||
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): 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,16 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.data.account.converter.AccountConverterFactoryContainer
|
||||
import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles
|
||||
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.featuretoggle.AccountsFeatureToggles
|
||||
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 +23,26 @@ internal object AccountDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountsCRUDRepository(userWalletsStore: UserWalletsStore): AccountsCRUDRepository {
|
||||
fun provideAccountFeatureToggle(featureTogglesManager: FeatureTogglesManager): AccountsFeatureToggles {
|
||||
return DefaultAccountsFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.data.account.featuretoggle
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
|
||||
internal class DefaultAccountsFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : AccountsFeatureToggles {
|
||||
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "ACCOUNTS_FEATURE_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.data.account.fetcher
|
||||
|
||||
import com.tangem.data.account.converter.CryptoPortfolioConverter
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||
import com.tangem.datasource.api.common.response.isNetworkError
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.Provider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Handles errors that occur during the fetching of wallet accounts
|
||||
*
|
||||
* @property userTokensSaver saves user tokens to the storage
|
||||
* @property userWalletsStore provides access to user wallet data
|
||||
* @property userTokensResponseStore provides access to user token responses.
|
||||
* @property cryptoPortfolioCF factory for converting crypto portfolios
|
||||
* @property userTokensResponseFactory factory for creating user token responses
|
||||
* @property cardCryptoCurrencyFactory factory for creating default cryptocurrencies for multi-currency wallets
|
||||
*
|
||||
* @see DefaultWalletAccountsFetcher
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory,
|
||||
private val userTokensResponseFactory: UserTokensResponseFactory,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Handles the error that occurred during the fetching of wallet accounts.
|
||||
* [pushWalletAccounts] and [storeWalletAccounts] are functions that passed as parameters to avoid
|
||||
* cyclic dependencies.
|
||||
*
|
||||
* @param error the error that occurred
|
||||
* @param userWalletId the ID of the user wallet
|
||||
* @param savedAccountsResponse the previously saved wallet accounts response, if available
|
||||
* @param pushWalletAccounts function to push wallet accounts to the server
|
||||
* @param storeWalletAccounts function to store wallet accounts locally
|
||||
*/
|
||||
suspend fun handle(
|
||||
error: ApiResponseError,
|
||||
userWalletId: UserWalletId,
|
||||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
pushWalletAccounts: suspend (userWalletId: UserWalletId, accounts: List<WalletAccountDTO>) -> Unit,
|
||||
storeWalletAccounts: suspend (userWalletId: UserWalletId, response: GetWalletAccountsResponse) -> Unit,
|
||||
) {
|
||||
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
|
||||
if (isResponseUpToDate) {
|
||||
Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
|
||||
return
|
||||
}
|
||||
|
||||
val userWalletProvider = Provider { userWalletsStore.getSyncStrict(key = userWalletId) }
|
||||
val accountDTOs = savedAccountsResponse?.accounts.orDefault(userWalletProvider = userWalletProvider)
|
||||
|
||||
val userTokensResponse = savedAccountsResponse?.toUserTokensResponse()
|
||||
.orFromLegacyStore(userWalletProvider = userWalletProvider)
|
||||
.orDefault(userWalletProvider = userWalletProvider)
|
||||
|
||||
val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND)
|
||||
if (isNotFoundError) {
|
||||
pushWalletAccounts(userWalletId, accountDTOs)
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse)
|
||||
}
|
||||
|
||||
val response = savedAccountsResponse.orDefault(userWalletId, accountDTOs, userTokensResponse)
|
||||
storeWalletAccounts(userWalletId, response)
|
||||
}
|
||||
|
||||
private fun List<WalletAccountDTO>?.orDefault(userWalletProvider: Provider<UserWallet>): List<WalletAccountDTO> {
|
||||
if (this != null) return this
|
||||
|
||||
val userWallet = userWalletProvider()
|
||||
|
||||
val accounts = AccountList.empty(userWallet).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
||||
val converter = cryptoPortfolioCF.create(userWallet = userWallet)
|
||||
|
||||
return converter.convertListBack(input = accounts)
|
||||
}
|
||||
|
||||
private suspend fun UserTokensResponse?.orFromLegacyStore(
|
||||
userWalletProvider: Provider<UserWallet>,
|
||||
): UserTokensResponse? {
|
||||
if (this != null) return this
|
||||
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
|
||||
return userTokensResponseStore.getSyncOrNull(userWalletId)
|
||||
.also { userTokensResponseStore.clear(userWalletId) }
|
||||
}
|
||||
|
||||
private fun UserTokensResponse?.orDefault(userWalletProvider: Provider<UserWallet>): UserTokensResponse {
|
||||
if (this != null) return this
|
||||
|
||||
return userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(
|
||||
userWallet = userWalletProvider(),
|
||||
),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun GetWalletAccountsResponse?.orDefault(
|
||||
userWalletId: UserWalletId,
|
||||
accountDTOs: List<WalletAccountDTO>,
|
||||
userTokensResponse: UserTokensResponse,
|
||||
): GetWalletAccountsResponse {
|
||||
if (this != null) return this
|
||||
|
||||
return GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = userTokensResponse.group,
|
||||
sort = userTokensResponse.sort,
|
||||
totalAccounts = accountDTOs.size,
|
||||
),
|
||||
accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = userTokensResponse.tokens),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +1,147 @@
|
|||
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)
|
||||
|
||||
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,57 @@
|
|||
package com.tangem.data.account.utils
|
||||
|
||||
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
|
||||
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.models.wallet.UserWalletId
|
||||
|
||||
/** Flattens the tokens from all wallet accounts into a single list */
|
||||
internal fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
|
||||
return accounts.flatMap { it.tokens.orEmpty() }
|
||||
}
|
||||
|
||||
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
|
||||
internal fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
|
||||
return UserTokensResponse(
|
||||
group = wallet.group,
|
||||
sort = wallet.sort,
|
||||
tokens = flattenTokens(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns tokens from a [UserTokensResponse] to the wallet accounts in the [GetWalletAccountsResponse]
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet
|
||||
*
|
||||
* @return a new [GetWalletAccountsResponse]` with tokens assigned to the wallet accounts
|
||||
*/
|
||||
internal fun GetWalletAccountsResponse.assignTokens(userWalletId: UserWalletId): GetWalletAccountsResponse {
|
||||
return copy(
|
||||
accounts = accounts.assignTokens(userWalletId = userWalletId, tokens = unassignedTokens),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns tokens from a [UserTokensResponse] to a list of wallet accounts
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet
|
||||
* @param tokens tokens to be assigned
|
||||
*
|
||||
* @return a new list of [WalletAccountDTO] with tokens assigned to each account
|
||||
*/
|
||||
internal fun List<WalletAccountDTO>.assignTokens(
|
||||
userWalletId: UserWalletId,
|
||||
tokens: List<UserTokensResponse.Token>,
|
||||
): List<WalletAccountDTO> {
|
||||
val enrichedTokens = UserTokensResponseAccountIdEnricher(userWalletId, tokens)
|
||||
.groupBy { it.accountId }
|
||||
|
||||
return map { accountDTO ->
|
||||
accountDTO.copy(
|
||||
tokens = enrichedTokens[accountDTO.id].orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,129 @@
|
|||
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,
|
||||
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,239 @@
|
|||
package com.tangem.data.account.fetcher
|
||||
|
||||
import com.tangem.data.account.converter.CryptoPortfolioConverter
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class FetchWalletAccountsErrorHandlerTest {
|
||||
|
||||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
|
||||
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory = mockk()
|
||||
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
|
||||
private val userTokensResponseFactory: UserTokensResponseFactory = mockk()
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
|
||||
|
||||
private val handler = FetchWalletAccountsErrorHandler(
|
||||
userTokensSaver = userTokensSaver,
|
||||
userWalletsStore = userWalletsStore,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
cryptoPortfolioCF = cryptoPortfolioCF,
|
||||
userTokensResponseFactory = userTokensResponseFactory,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
)
|
||||
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
clearMocks(
|
||||
userTokensSaver,
|
||||
userWalletsStore,
|
||||
userTokensResponseStore,
|
||||
cryptoPortfolioCF,
|
||||
cryptoPortfolioConverter,
|
||||
cardCryptoCurrencyFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not update accounts when response is up to date`() = runTest {
|
||||
// Arrange
|
||||
val error = ApiResponseError.HttpException(
|
||||
code = Code.NOT_MODIFIED,
|
||||
message = "Not Modified",
|
||||
errorBody = null,
|
||||
)
|
||||
|
||||
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk()
|
||||
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk()
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
error = error,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = null,
|
||||
pushWalletAccounts = pushWalletAccounts,
|
||||
storeWalletAccounts = storeWalletAccounts,
|
||||
)
|
||||
|
||||
// Assert
|
||||
coVerify(inverse = true) {
|
||||
userWalletsStore.getSyncStrict(key = any())
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId = any())
|
||||
userTokensResponseFactory.createUserTokensResponse(any(), any(), any())
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
|
||||
cryptoPortfolioCF.create(any())
|
||||
cryptoPortfolioConverter.convertListBack(any())
|
||||
pushWalletAccounts(any(), any())
|
||||
userTokensSaver.push(userWalletId = any(), response = any())
|
||||
storeWalletAccounts(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pushes and stores accounts when NOT_FOUND error occurs`() = runTest {
|
||||
// Arrange
|
||||
val error = ApiResponseError.HttpException(
|
||||
code = Code.NOT_FOUND,
|
||||
message = "Not Found",
|
||||
errorBody = null,
|
||||
)
|
||||
|
||||
val accountDTO = WalletAccountDTO(
|
||||
id = "nibh",
|
||||
name = "Michael Dotson",
|
||||
derivationIndex = 7135,
|
||||
icon = "consectetuer",
|
||||
iconColor = "ferri",
|
||||
tokens = listOf(),
|
||||
totalTokens = 7738,
|
||||
totalNetworks = 3348,
|
||||
)
|
||||
|
||||
val savedAccountsResponse = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(accountDTO),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk(relaxed = true)
|
||||
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
error = error,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = savedAccountsResponse,
|
||||
pushWalletAccounts = pushWalletAccounts,
|
||||
storeWalletAccounts = storeWalletAccounts,
|
||||
)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
pushWalletAccounts(userWalletId, listOf(accountDTO))
|
||||
userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
|
||||
storeWalletAccounts(userWalletId, savedAccountsResponse)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
userWalletsStore.getSyncStrict(key = any())
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId = any())
|
||||
userTokensResponseFactory.createUserTokensResponse(any(), any(), any())
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
|
||||
cryptoPortfolioCF.create(any())
|
||||
cryptoPortfolioConverter.convertListBack(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uses default accounts when savedAccountsResponse is null`() = runTest {
|
||||
// Arrange
|
||||
val error = ApiResponseError.TimeoutException
|
||||
|
||||
val accounts = AccountList.empty(userWallet).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
||||
val accountDTO = WalletAccountDTO(
|
||||
id = "nibh",
|
||||
name = "Michael Dotson",
|
||||
derivationIndex = 7135,
|
||||
icon = "consectetuer",
|
||||
iconColor = "ferri",
|
||||
tokens = listOf(),
|
||||
totalTokens = 7738,
|
||||
totalNetworks = 3348,
|
||||
)
|
||||
|
||||
val savedAccountsResponse = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(accountDTO),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
val userTokensResponse = savedAccountsResponse.toUserTokensResponse()
|
||||
|
||||
every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet
|
||||
every { cryptoPortfolioCF.create(userWallet) } returns cryptoPortfolioConverter
|
||||
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountDTO)
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId) } returns null
|
||||
every {
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = emptyList(),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
} returns userTokensResponse
|
||||
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList()
|
||||
|
||||
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk(relaxed = true)
|
||||
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
error = error,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = null,
|
||||
pushWalletAccounts = pushWalletAccounts,
|
||||
storeWalletAccounts = storeWalletAccounts,
|
||||
)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncStrict(userWalletId)
|
||||
cryptoPortfolioCF.create(userWallet)
|
||||
cryptoPortfolioConverter.convertListBack(accounts)
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId)
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = emptyList(),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet)
|
||||
storeWalletAccounts(userWalletId, any())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
pushWalletAccounts(any(), any())
|
||||
userTokensSaver.push(userWalletId = any(), response = any())
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
} 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)
|
||||
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())
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
package com.tangem.data.account.utils
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
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.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class GetWalletAccountsResponseExtTest {
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class FlattenTokens {
|
||||
|
||||
@Test
|
||||
fun `flattenTokens returns empty list when accounts are empty`() {
|
||||
// Arrange
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.flattenTokens()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flattenTokens returns empty list when single account has empty tokens`() {
|
||||
// Arrange
|
||||
val account = createWalletAccountDTO(derivationIndex = 0)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.flattenTokens()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flattenTokens returns all tokens from multiple accounts`() {
|
||||
// Arrange
|
||||
val token1 = createUserToken(id = "0")
|
||||
val token2 = createUserToken(id = "1")
|
||||
val token3 = createUserToken(id = "2")
|
||||
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0, tokens = listOf(token1, token2))
|
||||
val account2 = createWalletAccountDTO(derivationIndex = 1, tokens = listOf(token3))
|
||||
val account3 = createWalletAccountDTO(derivationIndex = 2)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
),
|
||||
accounts = listOf(account1, account2, account3),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.flattenTokens()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly(token1, token2, token3)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ToUserTokensResponse {
|
||||
|
||||
@Test
|
||||
fun `toUserTokensResponse returns correct UserTokensResponse for empty accounts and unassignedTokens`() {
|
||||
// Arrange
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.toUserTokensResponse()
|
||||
|
||||
// Assert
|
||||
val expected = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserTokensResponse includes tokens from accounts and unassignedTokens`() {
|
||||
// Arrange
|
||||
val token1 = createUserToken(id = "0")
|
||||
val token2 = createUserToken(id = "1")
|
||||
val account = createWalletAccountDTO(derivationIndex = 0, tokens = listOf(token1))
|
||||
val unassignedToken = token2
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = listOf(unassignedToken),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.toUserTokensResponse()
|
||||
|
||||
// Assert
|
||||
val expected = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
tokens = listOf(token1),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetWalletAccountsResponseAssignTokens {
|
||||
|
||||
@Test
|
||||
fun `assignTokens correctly assigns tokens to accounts`() {
|
||||
// Arrange
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val token1 = createUserToken(id = "0", accountId = null)
|
||||
val token2 = createUserToken(id = "1", accountId = null)
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0)
|
||||
val account2 = createWalletAccountDTO(derivationIndex = 1)
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
),
|
||||
accounts = listOf(account1, account2),
|
||||
unassignedTokens = listOf(token1, token2),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.assignTokens(userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = GetWalletAccountsResponse(
|
||||
wallet = response.wallet,
|
||||
accounts = listOf(
|
||||
account1.copy(
|
||||
tokens = listOf(
|
||||
token1.copy(accountId = accountId),
|
||||
token2.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
account2,
|
||||
),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assignTokens does not change accounts if there are no unassignedTokens`() {
|
||||
// Arrange
|
||||
val account = createWalletAccountDTO(derivationIndex = 0)
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.assignTokens(userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = GetWalletAccountsResponse(
|
||||
wallet = response.wallet,
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class WalletAccountDTOListAssignTokens {
|
||||
|
||||
@Test
|
||||
fun `assignTokens correctly assigns tokens to accounts`() {
|
||||
// Arrange
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val token1 = createUserToken(id = "0", accountId = null)
|
||||
val token2 = createUserToken(id = "1", accountId = null)
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0)
|
||||
val account2 = createWalletAccountDTO(derivationIndex = 1)
|
||||
|
||||
// Act
|
||||
val actual = listOf(account1, account2).assignTokens(
|
||||
userWalletId = userWalletId,
|
||||
tokens = listOf(token1, token2),
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
account1.copy(
|
||||
tokens = listOf(
|
||||
token1.copy(accountId = accountId),
|
||||
token2.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
account2,
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assignTokens does not change accounts if there are no unassignedTokens`() {
|
||||
// Arrange
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val token1 = createUserToken(id = "0", accountId = accountId)
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0)
|
||||
|
||||
// Act
|
||||
val actual = listOf(account1).assignTokens(
|
||||
userWalletId = userWalletId,
|
||||
tokens = listOf(token1),
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
account1.copy(
|
||||
tokens = listOf(
|
||||
token1.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWalletAccountDTO(derivationIndex: Int, tokens: List<UserTokensResponse.Token> = emptyList()) =
|
||||
WalletAccountDTO(
|
||||
id = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex(derivationIndex).getOrNull()!!,
|
||||
).value,
|
||||
name = "Name #$derivationIndex",
|
||||
derivationIndex = derivationIndex,
|
||||
icon = "icon",
|
||||
iconColor = "color",
|
||||
tokens = tokens,
|
||||
totalTokens = tokens.size,
|
||||
totalNetworks = 1,
|
||||
)
|
||||
|
||||
private fun createUserToken(id: String, accountId: String? = "account_id") = UserTokensResponse.Token(
|
||||
id = id,
|
||||
accountId = accountId,
|
||||
networkId = "ethereum",
|
||||
derivationPath = "m/44'/60'/0'/0/0",
|
||||
name = "Token",
|
||||
symbol = "T",
|
||||
contractAddress = "0x$id",
|
||||
decimals = 18,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
|
|
@ -19,19 +19,20 @@ dependencies {
|
|||
implementation(projects.core.utils)
|
||||
|
||||
/* Domain */
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.notifications.toggles)
|
||||
implementation(projects.domain.networks)
|
||||
implementation(projects.domain.wallets)
|
||||
|
||||
/* Libs - SDK */
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/* DI */
|
||||
|
|
|
|||
40
data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt
vendored
Normal file
40
data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.data.common.cache.etag
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of the [ETagsStore] interface for managing ETag values
|
||||
*
|
||||
* @property appPreferencesStore the preferences store used for saving and retrieving ETag values
|
||||
*/
|
||||
internal class DefaultETagsStore(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : ETagsStore {
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId, key: ETagsStore.Key): String? {
|
||||
val key = getAccountsETagKey(userWalletId = userWalletId, key = key)
|
||||
|
||||
return appPreferencesStore.getSyncOrNull(key = key)
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, key: ETagsStore.Key, value: String) {
|
||||
if (value.isBlank()) {
|
||||
Timber.e("ETag value is blank, not storing it. userWalletId: $userWalletId, key: $key")
|
||||
return
|
||||
}
|
||||
|
||||
val key = getAccountsETagKey(userWalletId = userWalletId, key = key)
|
||||
|
||||
appPreferencesStore.store(key = key, value = value)
|
||||
}
|
||||
|
||||
private fun getAccountsETagKey(userWalletId: UserWalletId, key: ETagsStore.Key): Preferences.Key<String> {
|
||||
return stringPreferencesKey(name = "etag_${key}_${userWalletId.stringValue}")
|
||||
}
|
||||
}
|
||||
34
data/common/src/main/kotlin/com/tangem/data/common/cache/etag/ETagsStore.kt
vendored
Normal file
34
data/common/src/main/kotlin/com/tangem/data/common/cache/etag/ETagsStore.kt
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.data.common.cache.etag
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Interface for working with ETag (Entity Tag), which is used for data caching and validation.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ETagsStore {
|
||||
|
||||
/**
|
||||
* Retrieves the stored ETag value for the specified wallet and key
|
||||
*
|
||||
* @param userWalletId identifier of the user wallet
|
||||
* @param key the key for which to get the ETag value
|
||||
*/
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId, key: Key): String?
|
||||
|
||||
/**
|
||||
* Stores the ETag value for the specified wallet and key
|
||||
*
|
||||
* @param userWalletId identifier of the user wallet
|
||||
* @param key the key for which to get the ETag value
|
||||
*/
|
||||
suspend fun store(userWalletId: UserWalletId, key: Key, value: String)
|
||||
|
||||
/** Enumeration of possible keys for storing ETag values */
|
||||
enum class Key {
|
||||
WalletAccounts,
|
||||
UserTokens,
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package com.tangem.data.common.currency
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Enriches the [UserTokensResponse] with accountId values for tokens
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object UserTokensResponseAccountIdEnricher {
|
||||
|
||||
/**
|
||||
* Enriches the tokens in the given [UserTokensResponse] with accountId values
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet
|
||||
* @param response the [UserTokensResponse] containing tokens to be enriched
|
||||
*/
|
||||
operator fun invoke(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse {
|
||||
val enrichedTokens = invoke(userWalletId = userWalletId, tokens = response.tokens)
|
||||
|
||||
return response.copy(tokens = enrichedTokens)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriches the given list of tokens with accountId values
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet
|
||||
* @param tokens the list of tokens to be enriched
|
||||
*/
|
||||
operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
tokens: List<UserTokensResponse.Token>,
|
||||
): List<UserTokensResponse.Token> {
|
||||
val hasUnassignedTokens = tokens.any { it.accountId == null }
|
||||
if (!hasUnassignedTokens) return tokens
|
||||
|
||||
val enrichedTokens = tokens
|
||||
.filter { it.accountId == null }
|
||||
.groupByAccountIndex()
|
||||
.mapKeysToAccountId(userWalletId)
|
||||
.mapToEnrichedTokens()
|
||||
|
||||
if (enrichedTokens.isEmpty()) return tokens
|
||||
|
||||
val enrichedTokenMap = enrichedTokens.associateBy { it }
|
||||
return tokens.map { token ->
|
||||
enrichedTokenMap[token] ?: token
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<UserTokensResponse.Token>.groupByAccountIndex(): Map<Long?, List<UserTokensResponse.Token>> {
|
||||
return this
|
||||
.groupBy { savedToken ->
|
||||
val derivationPathValue = savedToken.derivationPath
|
||||
if (derivationPathValue == null) {
|
||||
Timber.e("Token $savedToken has no derivation path")
|
||||
return@groupBy null
|
||||
}
|
||||
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId)
|
||||
if (blockchain == null) {
|
||||
Timber.e("Token $savedToken has unknown networkId")
|
||||
return@groupBy null
|
||||
}
|
||||
|
||||
val accountNodeRecognizer = AccountNodeRecognizer(blockchain)
|
||||
val accountIndex = accountNodeRecognizer.recognize(derivationPathValue)
|
||||
if (accountIndex == null) {
|
||||
Timber.e("Token $savedToken has unrecognized derivation path")
|
||||
return@groupBy null
|
||||
}
|
||||
|
||||
accountIndex
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<Long?, List<UserTokensResponse.Token>>.mapKeysToAccountId(
|
||||
userWalletId: UserWalletId,
|
||||
): Map<AccountId?, List<UserTokensResponse.Token>> {
|
||||
return mapKeys { (accountIndex, _) ->
|
||||
if (accountIndex == null) return@mapKeys null
|
||||
|
||||
val derivationIndex = DerivationIndex.invoke(value = accountIndex.toInt()).getOrElse {
|
||||
Timber.e("Failed to parse derivation index from account index: $accountIndex")
|
||||
return@mapKeys null
|
||||
}
|
||||
|
||||
AccountId.forCryptoPortfolio(userWalletId, derivationIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<AccountId?, List<UserTokensResponse.Token>>.mapToEnrichedTokens(): List<UserTokensResponse.Token> {
|
||||
return flatMap { (accountId, tokens) ->
|
||||
if (accountId == null) return@flatMap emptyList()
|
||||
|
||||
tokens.map { it.copy(accountId = accountId.value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import com.tangem.domain.models.network.NetworkStatus
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -15,17 +14,12 @@ import javax.inject.Inject
|
|||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class UserTokensResponseAddressesEnricher @Inject constructor(
|
||||
private val notificationsFeatureToggles: NotificationsFeatureToggles,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse {
|
||||
if (!notificationsFeatureToggles.isNotificationsEnabled) {
|
||||
return response
|
||||
}
|
||||
|
||||
val isNotificationsEnabled = walletsRepository.isNotificationsEnabled(userWalletId)
|
||||
|
||||
return withContext(dispatchers.default) {
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -5,60 +5,85 @@ import com.tangem.data.common.tokens.UserTokensBackwardCompatibility
|
|||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class UserTokensSaver(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val userTokensResponseAddressesEnricher: UserTokensResponseAddressesEnricher,
|
||||
private val addressesEnricher: UserTokensResponseAddressesEnricher,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
) {
|
||||
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse, useEnricher: Boolean = true) =
|
||||
withContext(dispatchers.io) {
|
||||
val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response)
|
||||
val enrichedUserTokensResponse = if (useEnricher) {
|
||||
userTokensResponseAddressesEnricher(
|
||||
userWalletId = userWalletId,
|
||||
response = compatibleUserTokensResponse,
|
||||
)
|
||||
} else {
|
||||
compatibleUserTokensResponse
|
||||
}
|
||||
|
||||
userTokensResponseStore.store(userWalletId = userWalletId, response = enrichedUserTokensResponse)
|
||||
}
|
||||
|
||||
suspend fun storeAndPush(userWalletId: UserWalletId, response: UserTokensResponse) {
|
||||
val enrichedUserTokensResponse = userTokensResponseAddressesEnricher(
|
||||
userWalletId = userWalletId,
|
||||
response = response,
|
||||
)
|
||||
store(userWalletId, enrichedUserTokensResponse, false)
|
||||
push(userWalletId, enrichedUserTokensResponse, false)
|
||||
withContext(dispatchers.default) {
|
||||
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = true)
|
||||
|
||||
store(userWalletId = userWalletId, response = enrichedResponse, useEnricher = false)
|
||||
push(userWalletId = userWalletId, response = enrichedResponse, useEnricher = false)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse, useEnricher: Boolean = true) =
|
||||
withContext(dispatchers.default) {
|
||||
val updatedResponse = response
|
||||
.applyCompatibility()
|
||||
.enrichIf(userWalletId = userWalletId, condition = useEnricher)
|
||||
|
||||
userTokensResponseStore.store(userWalletId = userWalletId, response = updatedResponse)
|
||||
}
|
||||
|
||||
suspend fun push(
|
||||
userWalletId: UserWalletId,
|
||||
response: UserTokensResponse,
|
||||
useEnricher: Boolean = true,
|
||||
onFailSend: () -> Unit = {},
|
||||
) = withContext(dispatchers.io) {
|
||||
val enrichedUserTokensResponse = if (useEnricher) {
|
||||
userTokensResponseAddressesEnricher(
|
||||
userWalletId = userWalletId,
|
||||
response = response,
|
||||
) {
|
||||
withContext(dispatchers.default) {
|
||||
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher)
|
||||
|
||||
safeApiCall(
|
||||
call = {
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = enrichedResponse)
|
||||
.bind()
|
||||
}
|
||||
},
|
||||
onError = { onFailSend() },
|
||||
)
|
||||
} else {
|
||||
response
|
||||
}
|
||||
safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedUserTokensResponse).bind() }) {
|
||||
Timber.e(it, "Unable to push user tokens for: ${userWalletId.stringValue}")
|
||||
onFailSend()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.applyCompatibility(): UserTokensResponse {
|
||||
return userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(userTokensResponse = this)
|
||||
}
|
||||
|
||||
private suspend fun UserTokensResponse.enrichIf(
|
||||
userWalletId: UserWalletId,
|
||||
condition: Boolean,
|
||||
): UserTokensResponse {
|
||||
if (!condition) return this
|
||||
|
||||
return this
|
||||
.enrichByAddress(userWalletId = userWalletId)
|
||||
.let {
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
it.enrichByAccountId(userWalletId = userWalletId)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun UserTokensResponse.enrichByAddress(userWalletId: UserWalletId): UserTokensResponse {
|
||||
return addressesEnricher(userWalletId = userWalletId, response = this)
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.enrichByAccountId(userWalletId: UserWalletId): UserTokensResponse {
|
||||
return UserTokensResponseAccountIdEnricher(userWalletId = userWalletId, response = this)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
package com.tangem.data.common.di
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.cache.etag.DefaultETagsStore
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.*
|
||||
import com.tangem.data.common.quote.DefaultQuotesFetcher
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -42,13 +45,11 @@ internal object DataCommonModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideUserTokensEncricher(
|
||||
notificationsFeatureToggles: NotificationsFeatureToggles,
|
||||
walletsRepository: WalletsRepository,
|
||||
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): UserTokensResponseAddressesEnricher {
|
||||
return UserTokensResponseAddressesEnricher(
|
||||
notificationsFeatureToggles = notificationsFeatureToggles,
|
||||
walletsRepository = walletsRepository,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
dispatchers = dispatchers,
|
||||
|
|
@ -61,13 +62,15 @@ internal object DataCommonModule {
|
|||
tangemTechApi: TangemTechApi,
|
||||
userTokensResponseStore: UserTokensResponseStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
enricher: UserTokensResponseAddressesEnricher,
|
||||
addressesEnricher: UserTokensResponseAddressesEnricher,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
): UserTokensSaver {
|
||||
return UserTokensSaver(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
dispatchers = dispatchers,
|
||||
userTokensResponseAddressesEnricher = enricher,
|
||||
addressesEnricher = addressesEnricher,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -76,4 +79,10 @@ internal object DataCommonModule {
|
|||
fun provideQuotesFetcher(tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider): QuotesFetcher {
|
||||
return DefaultQuotesFetcher(tangemTechApi = tangemTechApi, dispatchers = dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideETagsStore(appPreferencesStore: AppPreferencesStore): ETagsStore {
|
||||
return DefaultETagsStore(appPreferencesStore = appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package com.tangem.data.common.currency
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class UserTokensResponseAccountIdEnricherTest {
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val mockCryptoCurrencyFactory = MockCryptoCurrencyFactory()
|
||||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
|
||||
@Test
|
||||
fun `enriches tokens with missing account ids`() {
|
||||
// Arrange
|
||||
val response = mockCryptoCurrencyFactory.ethereumAndStellar
|
||||
.mapIndexed { index, currency ->
|
||||
currency.toResponseToken(
|
||||
accountId = null,
|
||||
derivationPath = "m/44'/60'/$index'/0/0",
|
||||
)
|
||||
}
|
||||
.toResponse()
|
||||
|
||||
// Act
|
||||
val actual = UserTokensResponseAccountIdEnricher(userWalletId, response)
|
||||
|
||||
// Assert
|
||||
val expected = response.tokens
|
||||
.mapIndexed { index, currency ->
|
||||
currency.enrichWithAccountId(accountIndex = index)
|
||||
}
|
||||
.toResponse()
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not modify tokens with existing account ids`() {
|
||||
// Arrange
|
||||
val response = mockCryptoCurrencyFactory.ethereumAndStellar
|
||||
.mapIndexed { index, currency ->
|
||||
currency.toResponseToken(derivationPath = "m/44'/60'/$index'/0/0")
|
||||
.enrichWithAccountId(accountIndex = index)
|
||||
}
|
||||
.toResponse()
|
||||
|
||||
// Act
|
||||
val actual = UserTokensResponseAccountIdEnricher(userWalletId, response)
|
||||
|
||||
// Assert
|
||||
val expected = response
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `skips tokens with invalid derivation paths`() {
|
||||
// Arrange
|
||||
val validDerivationPath = "m/44'/60'/0'/0/0"
|
||||
val invalidDerivationPath = "invalid/path"
|
||||
|
||||
val tokenWithInvalidPath = mockCryptoCurrencyFactory.ethereum.toResponseToken(
|
||||
accountId = null,
|
||||
derivationPath = invalidDerivationPath,
|
||||
)
|
||||
|
||||
val tokenWithValidPath = mockCryptoCurrencyFactory.stellar.toResponseToken(
|
||||
accountId = null,
|
||||
derivationPath = validDerivationPath,
|
||||
)
|
||||
|
||||
val response = listOf(tokenWithInvalidPath, tokenWithValidPath).toResponse()
|
||||
|
||||
// Act
|
||||
val actual = UserTokensResponseAccountIdEnricher(userWalletId, response)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
tokenWithInvalidPath,
|
||||
tokenWithValidPath.enrichWithAccountId(accountIndex = 0),
|
||||
).toResponse()
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `skips tokens with unknown network id`() {
|
||||
// Arrange
|
||||
val unknownNetworkId = "unknown"
|
||||
val validNetworkId = mockCryptoCurrencyFactory.ethereum.network.rawId
|
||||
|
||||
val tokenWithUnknownNetworkId = mockCryptoCurrencyFactory.ethereum.toResponseToken(
|
||||
networkId = unknownNetworkId,
|
||||
derivationPath = "m/44'/60'/0'/0/0",
|
||||
accountId = null,
|
||||
)
|
||||
|
||||
val tokenWithValidNetworkId = mockCryptoCurrencyFactory.ethereum.toResponseToken(
|
||||
accountId = null,
|
||||
networkId = validNetworkId,
|
||||
derivationPath = "m/44'/60'/0'/0/0",
|
||||
)
|
||||
|
||||
val response = listOf(tokenWithUnknownNetworkId, tokenWithValidNetworkId).toResponse()
|
||||
|
||||
// Act
|
||||
val actual = UserTokensResponseAccountIdEnricher(userWalletId, response)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
tokenWithUnknownNetworkId,
|
||||
tokenWithValidNetworkId.enrichWithAccountId(accountIndex = 0),
|
||||
).toResponse()
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.toResponseToken(
|
||||
accountId: AccountId? = null,
|
||||
networkId: String? = null,
|
||||
derivationPath: String,
|
||||
): UserTokensResponse.Token {
|
||||
return userTokensResponseFactory.createResponseToken(this).copy(
|
||||
networkId = networkId ?: network.rawId,
|
||||
derivationPath = derivationPath,
|
||||
accountId = accountId?.value,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<UserTokensResponse.Token>.toResponse(): UserTokensResponse {
|
||||
return UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
tokens = this,
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.Token.enrichWithAccountId(accountIndex: Int): UserTokensResponse.Token {
|
||||
val derivationIndex = DerivationIndex(value = accountIndex).getOrNull()!!
|
||||
val accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex)
|
||||
|
||||
return copy(accountId = accountId.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import com.tangem.domain.models.network.NetworkAddress
|
|||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
|
|
@ -23,7 +22,6 @@ import org.junit.Test
|
|||
|
||||
class UserTokensResponseAddressesEnricherTest {
|
||||
|
||||
private lateinit var notificationsFeatureToggles: NotificationsFeatureToggles
|
||||
private lateinit var walletsRepository: WalletsRepository
|
||||
private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider()
|
||||
private lateinit var multiNetworkStatusSupplier: MultiNetworkStatusSupplier
|
||||
|
|
@ -31,12 +29,10 @@ class UserTokensResponseAddressesEnricherTest {
|
|||
|
||||
@Before
|
||||
fun setup() {
|
||||
notificationsFeatureToggles = mockk()
|
||||
walletsRepository = mockk()
|
||||
multiNetworkStatusSupplier = mockk()
|
||||
|
||||
enricher = UserTokensResponseAddressesEnricher(
|
||||
notificationsFeatureToggles = notificationsFeatureToggles,
|
||||
walletsRepository = walletsRepository,
|
||||
dispatchers = dispatchers,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
|
|
@ -54,7 +50,6 @@ class UserTokensResponseAddressesEnricherTest {
|
|||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
every { notificationsFeatureToggles.isNotificationsEnabled } returns false
|
||||
|
||||
// WHEN
|
||||
val result = enricher(userWalletId, response)
|
||||
|
|
@ -70,7 +65,6 @@ class UserTokensResponseAddressesEnricherTest {
|
|||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
every { notificationsFeatureToggles.isNotificationsEnabled } returns true
|
||||
coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns false
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.invoke(any())
|
||||
|
|
@ -110,7 +104,6 @@ class UserTokensResponseAddressesEnricherTest {
|
|||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
val addresses = listOf("0x123", "0x456")
|
||||
|
||||
every { notificationsFeatureToggles.isNotificationsEnabled } returns true
|
||||
coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.invoke(any())
|
||||
|
|
@ -152,7 +145,6 @@ class UserTokensResponseAddressesEnricherTest {
|
|||
val token = createToken()
|
||||
val response = createUserTokensResponse(tokens = listOf(token))
|
||||
|
||||
every { notificationsFeatureToggles.isNotificationsEnabled } returns true
|
||||
coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true
|
||||
coEvery {
|
||||
multiNetworkStatusSupplier.invoke(any())
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError
|
|||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
|
|
@ -19,12 +20,16 @@ class UserTokensSaverTest {
|
|||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true)
|
||||
private val enricher: UserTokensResponseAddressesEnricher = mockk()
|
||||
private val accountsFeatureToggles = mockk<AccountsFeatureToggles> {
|
||||
every { this@mockk.isFeatureEnabled } returns true
|
||||
}
|
||||
|
||||
private val userTokensSaver: UserTokensSaver = UserTokensSaver(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
userTokensResponseAddressesEnricher = enricher,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
addressesEnricher = enricher,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region AndroidX libraries
|
||||
implementation(deps.androidx.datastore)
|
||||
// endregion
|
||||
|
|
@ -37,6 +36,8 @@ dependencies {
|
|||
|
||||
// endregion
|
||||
|
||||
// Feature modules
|
||||
implementation(projects.features.hotWallet.api)
|
||||
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.feedback.models)
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ import android.os.Build
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.navigation.email.EmailSender
|
||||
import com.tangem.data.feedback.converters.BlockchainInfoConverter
|
||||
import com.tangem.data.feedback.converters.CardInfoConverter
|
||||
import com.tangem.data.feedback.converters.WalletMetaInfoConverter
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.feedback.models.*
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
|
@ -23,28 +26,43 @@ import java.io.File
|
|||
*
|
||||
* @property appLogsStore app logs store
|
||||
* @property userWalletsListManager user wallets list manager
|
||||
* @property useNewUserWalletsRepository flag to use new user wallets repository
|
||||
* @property userWalletsListRepository user wallets repository
|
||||
* @property walletManagersStore wallet managers store
|
||||
* @property emailSender email sender
|
||||
* @property appVersionProvider app version provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultFeedbackRepository(
|
||||
private val appLogsStore: AppLogsStore,
|
||||
private val useNewUserWalletsRepository: Boolean,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val walletManagersStore: WalletManagersStore,
|
||||
private val emailSender: EmailSender,
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
) : FeedbackRepository {
|
||||
|
||||
private val blockchainsErrors = MutableStateFlow<Map<UserWalletId, BlockchainErrorInfo>>(emptyMap())
|
||||
|
||||
override fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse)
|
||||
override suspend fun getUserWalletMetaInfo(userWalletId: UserWalletId): WalletMetaInfo {
|
||||
val userWallet = getUserWalletById(userWalletId)
|
||||
return userWallet?.let {
|
||||
WalletMetaInfoConverter.convert(it)
|
||||
} ?: WalletMetaInfo(userWalletId)
|
||||
}
|
||||
|
||||
override fun getUserWalletMetaInfo(scanResponse: ScanResponse): WalletMetaInfo {
|
||||
return WalletMetaInfoConverter.convert(value = scanResponse)
|
||||
}
|
||||
|
||||
override fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo {
|
||||
return UserWalletsInfo(
|
||||
selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated",
|
||||
totalUserWallets = userWalletsListManager.walletsCount,
|
||||
totalUserWallets = totalUserWallets(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +95,7 @@ internal class DefaultFeedbackRepository(
|
|||
}
|
||||
|
||||
override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) {
|
||||
val userWallet = userWalletsListManager.selectedUserWalletSync ?: error("UserWallet is not selected")
|
||||
val userWallet = getSelectedWalletUseCase.sync().getOrNull() ?: error("UserWallet is not selected")
|
||||
|
||||
blockchainsErrors.update {
|
||||
it.toMutableMap().apply {
|
||||
|
|
@ -106,4 +124,20 @@ internal class DefaultFeedbackRepository(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getUserWalletById(userWalletId: UserWalletId): UserWallet? {
|
||||
return if (useNewUserWalletsRepository) {
|
||||
userWalletsListRepository.userWalletsSync().find { it.walletId == userWalletId }
|
||||
} else {
|
||||
userWalletsListManager.userWalletsSync.find { it.walletId == userWalletId }
|
||||
}
|
||||
}
|
||||
|
||||
private fun totalUserWallets(): Int {
|
||||
return if (useNewUserWalletsRepository) {
|
||||
userWalletsListRepository.userWallets.value?.size ?: 0
|
||||
} else {
|
||||
userWalletsListManager.walletsCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package com.tangem.data.feedback.converters
|
||||
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isVisa
|
||||
import com.tangem.domain.card.common.util.getBackupCardsCount
|
||||
import com.tangem.domain.feedback.models.CardInfo
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [ScanResponse] to [CardInfo]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object CardInfoConverter : Converter<ScanResponse, CardInfo> {
|
||||
|
||||
override fun convert(value: ScanResponse): CardInfo {
|
||||
return with(value) {
|
||||
CardInfo(
|
||||
userWalletId = createUserWalletId(scanResponse = value),
|
||||
cardId = card.cardId,
|
||||
cardsCount = value.getBackupCardsCount()?.toString() ?: "0",
|
||||
firmwareVersion = card.firmwareVersion.stringValue,
|
||||
cardBlockchain = walletData?.blockchain,
|
||||
signedHashesList = card.wallets.map {
|
||||
CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
|
||||
},
|
||||
isImported = value.card.wallets.any(CardDTO.Wallet::isImported),
|
||||
isStart2Coin = value.card.isStart2Coin,
|
||||
isVisa = value.card.isVisa,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId? {
|
||||
return UserWalletIdBuilder.scanResponse(scanResponse = scanResponse).build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.data.feedback.converters
|
||||
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isVisa
|
||||
import com.tangem.domain.card.common.util.getBackupCardsCount
|
||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [UserWallet] to [WalletMetaInfo]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object WalletMetaInfoConverter : Converter<UserWallet, WalletMetaInfo> {
|
||||
|
||||
override fun convert(value: UserWallet): WalletMetaInfo {
|
||||
return when (value) {
|
||||
is UserWallet.Cold -> {
|
||||
WalletMetaInfo(
|
||||
userWalletId = value.walletId,
|
||||
cardId = value.scanResponse.card.cardId,
|
||||
cardsCount = value.getBackupCardsCount()?.toString() ?: "0",
|
||||
firmwareVersion = value.scanResponse.card.firmwareVersion.stringValue,
|
||||
cardBlockchain = value.scanResponse.walletData?.blockchain,
|
||||
signedHashesList = value.scanResponse.card.wallets.map {
|
||||
WalletMetaInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
|
||||
},
|
||||
isImported = value.scanResponse.card.wallets.any(CardDTO.Wallet::isImported),
|
||||
isStart2Coin = value.scanResponse.card.isStart2Coin,
|
||||
isVisa = value.scanResponse.card.isVisa,
|
||||
)
|
||||
}
|
||||
is UserWallet.Hot -> {
|
||||
WalletMetaInfo(
|
||||
userWalletId = value.walletId,
|
||||
hotWalletIsBackedUp = value.backedUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun convert(value: ScanResponse): WalletMetaInfo {
|
||||
return WalletMetaInfo(
|
||||
userWalletId = createUserWalletId(value),
|
||||
cardId = value.card.cardId,
|
||||
cardsCount = value.getBackupCardsCount()?.toString() ?: "0",
|
||||
firmwareVersion = value.card.firmwareVersion.stringValue,
|
||||
cardBlockchain = value.walletData?.blockchain,
|
||||
signedHashesList = value.card.wallets.map {
|
||||
WalletMetaInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
|
||||
},
|
||||
isImported = value.card.wallets.any(CardDTO.Wallet::isImported),
|
||||
isStart2Coin = value.card.isStart2Coin,
|
||||
isVisa = value.card.isVisa,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId? {
|
||||
return UserWalletIdBuilder.scanResponse(scanResponse = scanResponse).build()
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,12 @@ import com.tangem.data.feedback.DefaultFeedbackFeatureToggles
|
|||
import com.tangem.data.feedback.DefaultFeedbackRepository
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -25,9 +28,12 @@ internal object FeedbackModule {
|
|||
fun provideFeedbackRepository(
|
||||
appLogsStore: AppLogsStore,
|
||||
userWalletsListManager: UserWalletsListManager,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
walletManagersStore: WalletManagersStore,
|
||||
emailSender: EmailSender,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
): FeedbackRepository {
|
||||
return DefaultFeedbackRepository(
|
||||
appLogsStore = appLogsStore,
|
||||
|
|
@ -35,6 +41,9 @@ internal object FeedbackModule {
|
|||
walletManagersStore = walletManagersStore,
|
||||
emailSender = emailSender,
|
||||
appVersionProvider = appVersionProvider,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
useNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
||||
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -261,6 +261,48 @@ internal class DefaultOnrampRepository(
|
|||
storeOnrampPairs(pairs = onrampPairs.await(), providers = providers.await())
|
||||
}
|
||||
|
||||
override suspend fun hasMercuryoSepaMethod(
|
||||
userWallet: UserWallet,
|
||||
currency: OnrampCurrency,
|
||||
country: OnrampCountry,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Boolean {
|
||||
return withContext(dispatchers.io) {
|
||||
val onrampPairs =
|
||||
safeApiCall(
|
||||
call = {
|
||||
onrampApi.getPairs(
|
||||
userWalletId = userWallet.walletId.stringValue,
|
||||
refCode = ExpressUtils.getRefCode(
|
||||
userWallet = userWallet,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
),
|
||||
body = OnrampPairsRequest(
|
||||
fromCurrencyCode = currency.code,
|
||||
countryCode = country.code,
|
||||
to = listOf(
|
||||
OnrampDestinationDTO(
|
||||
contractAddress = cryptoCurrency.getContractAddress(),
|
||||
network = cryptoCurrency.network.backendId,
|
||||
),
|
||||
),
|
||||
),
|
||||
).bind()
|
||||
},
|
||||
onError = {
|
||||
Timber.w(it, "Unable to fetch onramp pairs")
|
||||
throw it
|
||||
},
|
||||
)
|
||||
|
||||
val mercuryoProvider = onrampPairs.map { it.providers }.flatten()
|
||||
.find { it.providerId == MERCURYO_PROVIDER_ID }
|
||||
val hasSepaMethod = mercuryoProvider?.paymentMethods?.any { it == SEPA_METHOD_ID } ?: false
|
||||
|
||||
hasSepaMethod
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchQuotes(userWallet: UserWallet, cryptoCurrency: CryptoCurrency, amount: Amount) =
|
||||
withContext(dispatchers.io) {
|
||||
val pairs = requireNotNull(pairsStore.getSyncOrNull(PAIRS_KEY)) {
|
||||
|
|
@ -554,5 +596,8 @@ internal class DefaultOnrampRepository(
|
|||
const val PROVIDER_THEME_LIGHT = "light"
|
||||
|
||||
const val REDIRECT_URL = "https://tangem.com/onramp"
|
||||
|
||||
const val SEPA_METHOD_ID = "sepa"
|
||||
const val MERCURYO_PROVIDER_ID = "mercuryo"
|
||||
}
|
||||
}
|
||||
|
|
@ -38,24 +38,19 @@ internal class DefaultPromoRepository(
|
|||
key = PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name),
|
||||
default = true,
|
||||
).map { shouldShow ->
|
||||
if (promoId == PromoId.Referral) {
|
||||
runCatching {
|
||||
when (promoId) {
|
||||
PromoId.Referral -> runCatching {
|
||||
!referralRepository.isReferralParticipant(userWalletId) && shouldShow
|
||||
}.getOrDefault(false)
|
||||
} else {
|
||||
shouldShow
|
||||
PromoId.Sepa -> shouldShow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isReadyToShowTokenPromo(promoId: PromoId): Flow<Boolean> {
|
||||
return if (promoId == PromoId.Referral) {
|
||||
flowOf(false)
|
||||
} else {
|
||||
appPreferencesStore.get(
|
||||
PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name),
|
||||
default = false,
|
||||
)
|
||||
return when (promoId) {
|
||||
PromoId.Referral -> flowOf(false)
|
||||
PromoId.Sepa -> flowOf(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,15 +29,15 @@ internal class DefaultSettingsRepository(
|
|||
|
||||
private val userCountryFlow = MutableStateFlow<UserCountry?>(value = null)
|
||||
|
||||
override suspend fun shouldShowSaveUserWalletScreen(): Boolean {
|
||||
override suspend fun shouldShowAskBiometry(): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(
|
||||
key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY,
|
||||
key = PreferencesKeys.SHOULD_SHOW_ASK_BIOMETRY_KEY,
|
||||
default = true,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setShouldShowSaveUserWalletScreen(value: Boolean) {
|
||||
appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, value = value)
|
||||
override suspend fun setShouldShowAskBiometry(value: Boolean) {
|
||||
appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_ASK_BIOMETRY_KEY, value = value)
|
||||
}
|
||||
|
||||
override suspend fun isWalletScrollPreviewEnabled(): Boolean {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.tokens)
|
||||
|
||||
/** Project - Utils */
|
||||
implementation(projects.core.utils)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.data.pay.datasource
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTangemPayAuthDataSource @Inject constructor(
|
||||
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
) : TangemPayAuthDataSource {
|
||||
|
||||
override suspend fun generateNewAuthHeader(address: String, cardId: String): Either<Throwable, String> = either {
|
||||
val challenge = visaAuthRemoteDataSource
|
||||
.getCustomerWalletAuthChallenge(address)
|
||||
.mapLeft { IllegalStateException("TangemPay challenge failed. Error code: ${it.errorCode}") }
|
||||
.bind()
|
||||
|
||||
val signed = tangemSdkManager.visaCustomerWalletApprove(
|
||||
VisaDataForApprove(
|
||||
customerWalletCardId = cardId,
|
||||
targetAddress = address,
|
||||
dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge),
|
||||
),
|
||||
).toEither { IllegalStateException("TangemPay signing failed: $it") }.bind()
|
||||
|
||||
visaAuthRemoteDataSource.getTokenWithCustomerWallet(
|
||||
sessionId = challenge.session.sessionId,
|
||||
signature = signed.signature,
|
||||
nonce = signed.dataToSign.hashToSign,
|
||||
)
|
||||
.mapLeft { IllegalStateException("TangemPay token fetch failed. Error code: ${it.errorCode}") }
|
||||
.bind()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> CompletionResult<T>.toEither(map: (Throwable) -> Throwable) = when (this) {
|
||||
is CompletionResult.Success -> Either.Right(data)
|
||||
is CompletionResult.Failure -> Either.Left(map(error))
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.DefaultKycRepository
|
||||
import com.tangem.data.pay.repository.DefaultKycRepository
|
||||
import com.tangem.data.pay.usecase.DefaultKycStartInfoUseCase
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.usecase.KycStartInfoUseCase
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -14,5 +16,9 @@ internal interface TangemPayDataModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindKycRepositoryFactory(factory: DefaultKycRepository.Factory): KycRepository.Factory
|
||||
fun bindKycRepository(repository: DefaultKycRepository): KycRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindKycStartInfoUseCase(useCase: DefaultKycStartInfoUseCase): KycStartInfoUseCase
|
||||
}
|
||||
|
|
@ -1,58 +1,47 @@
|
|||
package com.tangem.data.pay
|
||||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.common.map
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultKycRepository @AssistedInject constructor(
|
||||
class DefaultKycRepository @Inject constructor(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val tangemPayStorage: TangemPayStorage,
|
||||
) : KycRepository {
|
||||
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
|
||||
override suspend fun getKycStartInfo(address: String, cardId: String): Either<UniversalError, KycStartInfo> {
|
||||
var authHeader = ""
|
||||
visaAuthRepository.getCustomerWalletAuthChallenge(address).getOrNull()?.let { result ->
|
||||
tangemSdkManager.visaCustomerWalletApprove(
|
||||
VisaDataForApprove(
|
||||
customerWalletCardId = cardId,
|
||||
targetAddress = address,
|
||||
dataToSign = VisaDataToSignByCustomerWallet(hashToSign = result.challenge),
|
||||
),
|
||||
).map { signResult ->
|
||||
visaAuthRepository.getTokenWithCustomerWallet(
|
||||
sessionId = result.session.sessionId,
|
||||
signature = signResult.signature,
|
||||
nonce = signResult.dataToSign.hashToSign,
|
||||
).getOrNull()?.let { authHeader = it }
|
||||
}
|
||||
}
|
||||
val authHeader = authDataSource.generateNewAuthHeader(address, cardId)
|
||||
.getOrNull()
|
||||
.takeIf { !it.isNullOrEmpty() }
|
||||
?: return Either.Left(VisaApiError.UnknownWithoutCode)
|
||||
tangemPayStorage.store(authHeader)
|
||||
return getKycInfo(authHeader)
|
||||
}
|
||||
|
||||
override suspend fun getKycStartInfo(authHeader: String): Either<UniversalError, KycStartInfo> {
|
||||
return getKycInfo(authHeader)
|
||||
}
|
||||
|
||||
private suspend fun getKycInfo(authHeader: String): Either<UniversalError, KycStartInfo> {
|
||||
return request {
|
||||
authHeader.ifEmpty { error("Cannot get auth header for KYC") }
|
||||
tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result
|
||||
}.map {
|
||||
KycStartInfo(
|
||||
token = it.token,
|
||||
locale = it.locale,
|
||||
)
|
||||
KycStartInfo(token = it.token, locale = it.locale)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,9 +64,4 @@ class DefaultKycRepository @AssistedInject constructor(
|
|||
return Either.Left(VisaApiError.UnknownWithoutCode)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : KycRepository.Factory {
|
||||
override fun create(): DefaultKycRepository
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.data.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.usecase.KycStartInfoUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* For TangemPay Customer Wallet auth we are using polygon address
|
||||
*/
|
||||
private const val POL_VALUE = "coin⟨POLYGON⟩polygon-ecosystem-token"
|
||||
|
||||
internal class DefaultKycStartInfoUseCase @Inject constructor(
|
||||
private val userWalletsRepository: UserWalletsListRepository,
|
||||
private val getCurrencyUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val kycRepository: KycRepository,
|
||||
) : KycStartInfoUseCase {
|
||||
|
||||
override suspend fun invoke(): Either<UniversalError, KycStartInfo> = either {
|
||||
val wallet = userWalletsRepository.userWalletsSync().find { it is UserWallet.Cold } as? UserWallet.Cold
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
|
||||
val address = getCurrencyUseCase.invokeMultiWalletSync(wallet.walletId, CryptoCurrency.ID.fromValue(POL_VALUE))
|
||||
.getOrNull()?.value?.networkAddress?.defaultAddress?.value ?: raise(VisaApiError.UnknownWithoutCode)
|
||||
|
||||
kycRepository.getKycStartInfo(address, wallet.cardId).bind()
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
|||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -32,7 +32,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
private val visaApi: TangemPayApi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
|
||||
private val visaLibLoader: VisaLibLoader,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
) : VisaActivationRepository {
|
||||
|
|
@ -194,7 +194,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
}
|
||||
|
||||
val authTokens = visaAuthTokenStorage.get(visaCardId.cardId) ?: error("Auth tokens are not stored")
|
||||
val newTokens = visaAuthRepository.refreshAccessTokens(authTokens.refreshToken).getOrElse {
|
||||
val newTokens = visaAuthRemoteDataSource.refreshAccessTokens(authTokens.refreshToken).getOrElse {
|
||||
return Either.Left(VisaApiError.RefreshTokenExpired)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,17 +13,16 @@ import com.tangem.domain.visa.model.VisaAuthChallenge
|
|||
import com.tangem.domain.visa.model.VisaAuthSession
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
internal class DefaultVisaAuthRepository @Inject constructor(
|
||||
internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val visaAuthApi: TangemPayApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : VisaAuthRepository {
|
||||
) : VisaAuthRemoteDataSource {
|
||||
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.pay.datasource.DefaultTangemPayAuthDataSource
|
||||
import com.tangem.data.visa.DefaultVisaActivationRepository
|
||||
import com.tangem.data.visa.DefaultVisaAuthRepository
|
||||
import com.tangem.data.visa.DefaultVisaAuthRemoteDataSource
|
||||
import com.tangem.data.visa.MockVisaRepository
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -18,7 +20,7 @@ internal interface VisaDataModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository
|
||||
fun bindVisaAuthRemoteDataSource(repository: DefaultVisaAuthRemoteDataSource): VisaAuthRemoteDataSource
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
|
|
@ -39,4 +41,8 @@ internal interface VisaDataModule {
|
|||
// Mocked
|
||||
@Binds
|
||||
fun bindVisaRepository(repository: MockVisaRepository): VisaRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayAuthDataSource(repository: DefaultTangemPayAuthDataSource): TangemPayAuthDataSource
|
||||
}
|
||||
|
|
@ -21,12 +21,9 @@ import com.tangem.datasource.di.SdkMoshi
|
|||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.datasource.local.walletconnect.WalletConnectStore
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.walletconnect.WcPairService
|
||||
import com.tangem.domain.walletconnect.WcRequestService
|
||||
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
|
||||
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
|
||||
import com.tangem.domain.walletconnect.repository.WalletConnectRepository
|
||||
import com.tangem.domain.walletconnect.repository.WcSessionsManager
|
||||
import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase
|
||||
|
|
@ -90,7 +87,6 @@ internal object WalletConnectDataModule {
|
|||
fun defaultWcSessionsManager(
|
||||
store: WalletConnectStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
legacyStore: WalletConnectSessionsRepository,
|
||||
getWallets: GetWalletsUseCase,
|
||||
wcNetworksConverter: WcNetworksConverter,
|
||||
analytics: AnalyticsEventHandler,
|
||||
|
|
@ -99,7 +95,6 @@ internal object WalletConnectDataModule {
|
|||
return DefaultWcSessionsManager(
|
||||
store = store,
|
||||
dispatchers = dispatchers,
|
||||
legacyStore = legacyStore,
|
||||
getWallets = getWallets,
|
||||
wcNetworksConverter = wcNetworksConverter,
|
||||
analytics = analytics,
|
||||
|
|
@ -177,15 +172,11 @@ internal object WalletConnectDataModule {
|
|||
fun wcNetworksConverter(
|
||||
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
tokensFeatureToggles: TokensFeatureToggles,
|
||||
): WcNetworksConverter = WcNetworksConverter(
|
||||
namespaceConverters = namespaceConverters,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
currenciesRepository = currenciesRepository,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
tokensFeatureToggles = tokensFeatureToggles,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
|
@ -193,15 +184,11 @@ internal object WalletConnectDataModule {
|
|||
fun associateNetworksDelegate(
|
||||
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
|
||||
getWallets: GetWalletsUseCase,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
tokensFeatureToggles: TokensFeatureToggles,
|
||||
): AssociateNetworksDelegate = AssociateNetworksDelegate(
|
||||
namespaceConverters = namespaceConverters,
|
||||
getWallets = getWallets,
|
||||
currenciesRepository = currenciesRepository,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
tokensFeatureToggles = tokensFeatureToggles,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.walletconnect.model.WcPairError
|
||||
import com.tangem.domain.walletconnect.model.WcSessionProposal.ProposalNetwork
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
|
|
@ -19,9 +17,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
|||
internal class AssociateNetworksDelegate(
|
||||
private val namespaceConverters: Set<WcNamespaceConverter>,
|
||||
private val getWallets: GetWalletsUseCase,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
private val tokensFeatureToggles: TokensFeatureToggles,
|
||||
) {
|
||||
|
||||
@Throws(WcPairError.UnsupportedBlockchains::class)
|
||||
|
|
@ -96,14 +92,10 @@ internal class AssociateNetworksDelegate(
|
|||
}
|
||||
|
||||
private suspend fun getWalletNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
return if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
|
||||
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
.orEmpty()
|
||||
} else {
|
||||
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
|
||||
}
|
||||
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
|
||||
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
.orEmpty()
|
||||
.filterIsInstance<CryptoCurrency.Coin>()
|
||||
.map(CryptoCurrency.Coin::network)
|
||||
// flatten all derivation
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.data.walletconnect.sessions
|
|||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.domain.blockaid.models.dapp.CheckDAppResult
|
||||
import com.reown.walletkit.client.Wallet
|
||||
import com.reown.walletkit.client.WalletKit
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -16,7 +15,6 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.walletconnect.WcAnalyticEvents
|
||||
import com.tangem.domain.walletconnect.model.WcSession
|
||||
import com.tangem.domain.walletconnect.model.WcSessionDTO
|
||||
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
|
||||
import com.tangem.domain.walletconnect.repository.WcSessionsManager
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -30,7 +28,6 @@ import kotlin.coroutines.resume
|
|||
@Suppress("LongParameterList")
|
||||
internal class DefaultWcSessionsManager(
|
||||
private val store: WalletConnectStore,
|
||||
private val legacyStore: WalletConnectSessionsRepository,
|
||||
private val getWallets: GetWalletsUseCase,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val wcNetworksConverter: WcNetworksConverter,
|
||||
|
|
@ -39,18 +36,12 @@ internal class DefaultWcSessionsManager(
|
|||
) : WcSessionsManager, WcSdkObserver {
|
||||
|
||||
private val onSessionDelete = Channel<Wallet.Model.SessionDelete>(capacity = Channel.BUFFERED)
|
||||
private val oneTimeMigration = MutableStateFlow(true)
|
||||
|
||||
override val sessions: Flow<Map<UserWallet, List<WcSession>>>
|
||||
get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore }
|
||||
.transform { pair ->
|
||||
val (wallets, inStore) = pair
|
||||
val inSdk: List<Wallet.Model.Session> = WalletKit.getListOfActiveSessions()
|
||||
if (oneTimeMigration.value) {
|
||||
oneTimeMigration.value = false
|
||||
val someMigrated = migrateLegacyStore(inStore, inSdk, wallets)
|
||||
if (someMigrated) return@transform // ignore emit, wait next one
|
||||
}
|
||||
val associatedSessions: List<WcSession> = associate(inSdk, inStore, wallets)
|
||||
val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions)
|
||||
if (someRemove) return@transform // ignore emit, wait next one
|
||||
|
|
@ -60,7 +51,6 @@ internal class DefaultWcSessionsManager(
|
|||
.flowOn(dispatchers.io)
|
||||
|
||||
override fun onWcSdkInit() {
|
||||
oneTimeMigration.value = true
|
||||
listenOnSessionDelete()
|
||||
extendSessions()
|
||||
}
|
||||
|
|
@ -96,39 +86,6 @@ internal class DefaultWcSessionsManager(
|
|||
onSessionDelete.trySend(sessionDelete)
|
||||
}
|
||||
|
||||
private suspend fun migrateLegacyStore(
|
||||
inNewStore: Set<WcSessionDTO>,
|
||||
inSdk: List<Wallet.Model.Session>,
|
||||
wallets: List<UserWallet>,
|
||||
): Boolean {
|
||||
val walletIds = wallets.map { wallet -> wallet.walletId }
|
||||
val inLegacyStore = walletIds
|
||||
.map { walletId ->
|
||||
flow {
|
||||
emit(
|
||||
legacyStore.loadSessions(walletId.stringValue).mapNotNull { legacySession ->
|
||||
val url =
|
||||
inSdk.find { it.topic == legacySession.topic }?.metaData?.url ?: return@mapNotNull null
|
||||
WcSessionDTO(
|
||||
topic = legacySession.topic,
|
||||
walletId = walletId,
|
||||
url = url,
|
||||
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
.merge()
|
||||
.reduce { accumulator, value -> accumulator.plus(value) }
|
||||
// migrate only active legacySessions
|
||||
.filter { legacySession -> inSdk.any { inSdkSession -> inSdkSession.topic == legacySession.topic } }
|
||||
|
||||
val mustSaveInNewStore = inLegacyStore.subtract(inNewStore)
|
||||
if (mustSaveInNewStore.isNotEmpty()) store.saveSessions(mustSaveInNewStore)
|
||||
return mustSaveInNewStore.isNotEmpty()
|
||||
}
|
||||
|
||||
private suspend fun associate(
|
||||
inSdk: List<Wallet.Model.Session>,
|
||||
inStore: Set<WcSessionDTO>,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.walletconnect.model.WcSession
|
||||
import com.tangem.domain.walletconnect.model.WcSessionApprove
|
||||
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
|
||||
|
|
@ -20,9 +18,7 @@ import javax.inject.Inject
|
|||
internal class WcNetworksConverter @Inject constructor(
|
||||
private val namespaceConverters: Set<WcNamespaceConverter>,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
private val tokensFeatureToggles: TokensFeatureToggles,
|
||||
) {
|
||||
|
||||
fun createNetwork(chainId: String, wallet: UserWallet): Network? {
|
||||
|
|
@ -101,12 +97,10 @@ internal class WcNetworksConverter @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun getWalletNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
return if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
|
||||
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
|
||||
).orEmpty()
|
||||
} else {
|
||||
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
|
||||
}.filterIsInstance<CryptoCurrency.Coin>().map(CryptoCurrency.Coin::network)
|
||||
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
|
||||
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
.orEmpty()
|
||||
.filterIsInstance<CryptoCurrency.Coin>().map(CryptoCurrency.Coin::network)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,30 +3,72 @@ 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
|
||||
import com.tangem.hot.sdk.exception.WrongPasswordException
|
||||
import com.tangem.hot.sdk.model.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.set
|
||||
|
||||
class HotWalletAccessor @Inject constructor(
|
||||
class DefaultHotWalletAccessor @Inject constructor(
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
) {
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) : HotWalletAccessor {
|
||||
|
||||
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> =
|
||||
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
|
||||
|
||||
private var contextualUnlockHotWallet: ConcurrentHashMap<HotWalletId, UnlockHotWallet?> = ConcurrentHashMap()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun exportSeedPhrase(hotWalletId: HotWalletId): SeedPhrasePrivateInfo {
|
||||
val unlockHotWallet = contextualUnlockHotWallet[hotWalletId] ?: hotSdkRequest(hotWalletId) { it }
|
||||
return tangemHotSdk.exportMnemonic(unlockHotWallet = unlockHotWallet)
|
||||
}
|
||||
|
||||
override suspend fun unlockContextual(hotWalletId: HotWalletId): UnlockHotWallet = hotSdkRequest(hotWalletId) {
|
||||
tangemHotSdk.getContextUnlock(it).also { unlockHotWallet ->
|
||||
contextualUnlockHotWallet[hotWalletId] = unlockHotWallet
|
||||
}
|
||||
}
|
||||
|
||||
override fun getContextualUnlock(hotWalletId: HotWalletId): UnlockHotWallet? =
|
||||
contextualUnlockHotWallet[hotWalletId]
|
||||
|
||||
override fun clearContextualUnlock(hotWalletId: HotWalletId) {
|
||||
contextualUnlockHotWallet[hotWalletId] = null
|
||||
scope.launch {
|
||||
tangemHotSdk.clearUnlockContext(hotWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearAllContextualUnlock() {
|
||||
val hotWalletsIds = contextualUnlockHotWallet.keys.toList()
|
||||
contextualUnlockHotWallet.clear()
|
||||
scope.launch {
|
||||
hotWalletsIds.forEach { tangemHotSdk.clearUnlockContext(it) }
|
||||
}
|
||||
}
|
||||
|
||||
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