Updated on 2026-08-14
This commit is contained in:
commit
9aacdb2ed3
695 changed files with 22022 additions and 5116 deletions
|
|
@ -15,9 +15,14 @@ tasks.withType<Test>().configureEach {
|
|||
|
||||
dependencies {
|
||||
|
||||
// region Project - Common
|
||||
implementation(projects.common.ui) // It's needed for getting AccountName.DefaultMain value
|
||||
// endregion
|
||||
|
||||
// region Project - Core
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.res)
|
||||
api(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
|
|
@ -27,6 +32,7 @@ dependencies {
|
|||
api(projects.domain.common)
|
||||
api(projects.domain.models)
|
||||
api(projects.domain.tokens)
|
||||
api(projects.domain.wallets)
|
||||
// endregion
|
||||
|
||||
// region Project - Data
|
||||
|
|
|
|||
|
|
@ -7,21 +7,28 @@ 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 accountsListCF factory for creating an account list converter
|
||||
* @property cryptoPortfolioCF factory for creating a crypto portfolio converter
|
||||
* @property userWalletsStore store for accessing user wallet data
|
||||
*
|
||||
* @constructor Creates an instance of the container with injected factories.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class AccountConverterFactoryContainer @Inject constructor(
|
||||
val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory,
|
||||
private val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory,
|
||||
private val accountsListCF: AccountListConverter.Factory,
|
||||
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
) {
|
||||
|
||||
fun createWalletAccountsResponseConverter(userWalletId: UserWalletId): GetWalletAccountsResponseConverter {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
|
||||
|
||||
return getWalletAccountsResponseCF.create(userWallet)
|
||||
}
|
||||
|
||||
fun createAccountListConverter(userWalletId: UserWalletId): AccountListConverter {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ internal class AccountListConverter @AssistedInject constructor(
|
|||
override fun convert(value: GetWalletAccountsResponse): AccountList {
|
||||
return AccountList(
|
||||
userWalletId = userWallet.walletId,
|
||||
accounts = value.accounts.map(cryptoPortfolioConverter::convert).toSet(),
|
||||
accounts = value.accounts.map(cryptoPortfolioConverter::convert),
|
||||
totalAccounts = value.wallet.totalAccounts,
|
||||
sortType = TokensSortTypeConverter.convert(value.wallet.sort),
|
||||
groupType = TokensGroupTypeConverter.convert(value.wallet.group),
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ internal class CryptoPortfolioConverter @AssistedInject constructor(
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = tokens,
|
||||
userWallet = userWallet,
|
||||
accountIndex = value.derivationIndex.toDerivationIndex(),
|
||||
).toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.data.account.converter.AccountConverterFactoryContainer
|
||||
import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles
|
||||
|
|
@ -10,9 +11,9 @@ import com.tangem.data.account.store.ArchivedAccountsStoreFactory
|
|||
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
|
|
@ -21,6 +22,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -42,8 +44,8 @@ internal object AccountDataModule {
|
|||
accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
userTokensSaver: UserTokensSaver,
|
||||
eTagsStore: ETagsStore,
|
||||
accountConverterFactoryContainer: AccountConverterFactoryContainer,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AccountsCRUDRepository {
|
||||
return DefaultAccountsCRUDRepository(
|
||||
|
|
@ -53,8 +55,9 @@ internal object AccountDataModule {
|
|||
archivedAccountsStoreFactory = ArchivedAccountsStoreFactory,
|
||||
userWalletsStore = userWalletsStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
eTagsStore = eTagsStore,
|
||||
archivedAccountsETagStore = RuntimeStateStore(emptyMap()),
|
||||
convertersContainer = accountConverterFactoryContainer,
|
||||
resources = context.resources,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import com.tangem.data.account.producer.DefaultSingleAccountProducer
|
||||
import com.tangem.domain.account.producer.SingleAccountProducer
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface SingleAccountProducerFactoryModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSingleAccountProducerFactory(impl: DefaultSingleAccountProducer.Factory): SingleAccountProducer.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import com.tangem.domain.account.producer.SingleAccountProducer
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object SingleAccountSupplierModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSingleAccountSupplier(factory: SingleAccountProducer.Factory): SingleAccountSupplier {
|
||||
return object : SingleAccountSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { "single_account_${it.accountId.value}" },
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import com.tangem.data.account.store.AccountsResponseStore
|
|||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
|
|
@ -19,6 +18,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
|||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.datasource.utils.getSyncOrNull
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -51,18 +51,25 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : WalletAccountsFetcher, WalletAccountsSaver {
|
||||
|
||||
override suspend fun fetch(userWalletId: UserWalletId) {
|
||||
override suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse {
|
||||
val savedAccountsResponse = getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
|
||||
val accountsResponse = fetchWalletAccounts(userWalletId, savedAccountsResponse)
|
||||
?: return
|
||||
|
||||
if (accountsResponse.accounts.isEmpty()) {
|
||||
initializeAccounts(userWalletId, accountsResponse)
|
||||
} else if (accountsResponse.unassignedTokens.isNotEmpty()) {
|
||||
assignTokens(userWalletId, accountsResponse)
|
||||
return when {
|
||||
accountsResponse.accounts.isEmpty() -> {
|
||||
initializeAccounts(userWalletId, accountsResponse)
|
||||
}
|
||||
accountsResponse.unassignedTokens.isNotEmpty() -> {
|
||||
assignTokens(userWalletId, accountsResponse)
|
||||
}
|
||||
else -> accountsResponse
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? {
|
||||
return getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
|
||||
val store = getAccountsResponseStore(userWalletId = userWalletId)
|
||||
|
||||
|
|
@ -115,7 +122,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
private suspend fun fetchWalletAccounts(
|
||||
userWalletId: UserWalletId,
|
||||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
): GetWalletAccountsResponse? {
|
||||
): GetWalletAccountsResponse {
|
||||
return safeApiCall(
|
||||
call = {
|
||||
val apiResponse = withContext(dispatchers.io) {
|
||||
|
|
@ -145,7 +152,10 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun initializeAccounts(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
|
||||
private suspend fun initializeAccounts(
|
||||
userWalletId: UserWalletId,
|
||||
accountsResponse: GetWalletAccountsResponse,
|
||||
): GetWalletAccountsResponse {
|
||||
val response = defaultWalletAccountsResponseFactory.create(
|
||||
userWalletId = userWalletId,
|
||||
userTokensResponse = UserTokensResponse(
|
||||
|
|
@ -156,14 +166,17 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
)
|
||||
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = response.toUserTokensResponse())
|
||||
val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts)
|
||||
val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts) ?: response
|
||||
|
||||
if (syncedResponse != null) {
|
||||
store(userWalletId = userWalletId, response = syncedResponse)
|
||||
}
|
||||
store(userWalletId = userWalletId, response = syncedResponse)
|
||||
|
||||
return syncedResponse
|
||||
}
|
||||
|
||||
private suspend fun assignTokens(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
|
||||
private suspend fun assignTokens(
|
||||
userWalletId: UserWalletId,
|
||||
accountsResponse: GetWalletAccountsResponse,
|
||||
): GetWalletAccountsResponse {
|
||||
val accountsResponseWithTokens = accountsResponse.assignTokens(userWalletId)
|
||||
|
||||
store(userWalletId = userWalletId, response = accountsResponseWithTokens)
|
||||
|
|
@ -172,6 +185,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
userWalletId = userWalletId,
|
||||
response = accountsResponseWithTokens.toUserTokensResponse(),
|
||||
)
|
||||
|
||||
return accountsResponseWithTokens
|
||||
}
|
||||
|
||||
private suspend fun getETag(userWalletId: UserWalletId): String? {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,50 @@
|
|||
package com.tangem.data.account.fetcher
|
||||
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
||||
import com.tangem.datasource.api.common.response.isNetworkError
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.OnlyWalletIdBody
|
||||
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.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Handles errors that occur during the fetching of wallet accounts
|
||||
*
|
||||
* @property tangemTechApi API for network requests
|
||||
* @property userTokensSaver saves user tokens to the storage
|
||||
* @property userTokensResponseStore provides access to user token responses.
|
||||
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
|
||||
* @property eTagsStore store for ETags to manage caching
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
* @see DefaultWalletAccountsFetcher
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Singleton
|
||||
internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
|
||||
private val eTagsStore: ETagsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -49,23 +64,27 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse?,
|
||||
storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit,
|
||||
): GetWalletAccountsResponse? {
|
||||
): GetWalletAccountsResponse {
|
||||
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
|
||||
if (isResponseUpToDate) {
|
||||
Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
|
||||
return savedAccountsResponse
|
||||
return requireNotNull(savedAccountsResponse) {
|
||||
"Saved accounts response is null for wallet: $userWalletId"
|
||||
}
|
||||
}
|
||||
|
||||
var response = savedAccountsResponse ?: createDefaultResponse(userWalletId)
|
||||
val response = savedAccountsResponse ?: createDefaultResponse(userWalletId)
|
||||
val (accountDTOs, userTokensResponse) = response.accounts to response.toUserTokensResponse()
|
||||
|
||||
val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND)
|
||||
if (isNotFoundError) {
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse)
|
||||
val updatedResponse = pushWalletAccounts(userWalletId, accountDTOs)
|
||||
val eTag = createWallet(userWalletId)
|
||||
|
||||
if (updatedResponse != null) {
|
||||
response = updatedResponse
|
||||
if (eTag != null) {
|
||||
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = eTag)
|
||||
|
||||
pushWalletAccounts(userWalletId, accountDTOs)
|
||||
userTokensSaver.push(userWalletId, userTokensResponse)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,4 +109,24 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
}
|
||||
.also { userTokensResponseStore.clear(userWalletId) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wallet on the server and returns the ETag if successful.
|
||||
*
|
||||
* @param userWalletId The ID of the user wallet to create.
|
||||
|
||||
*/
|
||||
private suspend fun createWallet(userWalletId: UserWalletId): String? {
|
||||
val creationResponse = withContext(dispatchers.io) {
|
||||
tangemTechApi.createWallet(
|
||||
body = OnlyWalletIdBody(walletId = userWalletId.stringValue),
|
||||
)
|
||||
}
|
||||
|
||||
return if (creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED) {
|
||||
creationResponse.headers[ETAG_HEADER]?.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,9 @@ package com.tangem.data.account.producer
|
|||
import arrow.core.Option
|
||||
import arrow.core.some
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
|
|
@ -48,12 +48,16 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor(
|
|||
.map { response ->
|
||||
if (response == null) return@map emptySet()
|
||||
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = response.toUserTokensResponse(),
|
||||
userWallet = userWallet,
|
||||
).toSet()
|
||||
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = accountDTO.tokens.orEmpty(),
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.onEmpty { emit(emptySet()) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.data.account.producer
|
||||
|
||||
import arrow.core.Option
|
||||
import arrow.core.none
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.producer.SingleAccountProducer
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
/**
|
||||
* Default implementation of [SingleAccountProducer] that produces a flow of [Account.CryptoPortfolio]
|
||||
* for a single account identified by [SingleAccountProducer.Params.accountId].
|
||||
*
|
||||
* It uses [SingleAccountListSupplier] to get the list of accounts and filters it to find the
|
||||
* specific account.
|
||||
*
|
||||
* @property params Parameters containing the account ID for which the portfolio is produced.
|
||||
* @property singleAccountListSupplier Supplier to get the list of accounts.
|
||||
* @property dispatchers Coroutine dispatcher provider for managing threading.
|
||||
*/
|
||||
internal class DefaultSingleAccountProducer @AssistedInject constructor(
|
||||
@Assisted val params: SingleAccountProducer.Params,
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SingleAccountProducer {
|
||||
|
||||
override val fallback: Option<Account.CryptoPortfolio>
|
||||
get() = none()
|
||||
|
||||
override fun produce(): Flow<Account.CryptoPortfolio> {
|
||||
return singleAccountListSupplier(
|
||||
params = SingleAccountListProducer.Params(userWalletId = params.accountId.userWalletId),
|
||||
)
|
||||
.mapNotNull { accountList ->
|
||||
accountList.accounts.firstOrNull {
|
||||
it is Account.CryptoPortfolio && params.accountId == it.accountId
|
||||
} as? Account.CryptoPortfolio
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SingleAccountProducer.Factory {
|
||||
override fun create(params: SingleAccountProducer.Params): DefaultSingleAccountProducer
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,27 @@
|
|||
package com.tangem.data.account.repository
|
||||
|
||||
import android.content.res.Resources
|
||||
import arrow.core.Option
|
||||
import arrow.core.raise.option
|
||||
import arrow.core.toOption
|
||||
import com.tangem.common.ui.account.AccountNameUM
|
||||
import com.tangem.core.res.getStringSafe
|
||||
import com.tangem.data.account.converter.AccountConverterFactoryContainer
|
||||
import com.tangem.data.account.converter.ArchivedAccountConverter
|
||||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.store.ArchivedAccountsStore
|
||||
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
|
||||
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.datasource.utils.getSyncOrNull
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
|
|
@ -23,6 +29,7 @@ import com.tangem.domain.account.models.ArchivedAccount
|
|||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
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 com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -43,8 +50,9 @@ internal class DefaultAccountsCRUDRepository(
|
|||
private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val eTagsStore: ETagsStore,
|
||||
private val archivedAccountsETagStore: RuntimeStateStore<Map<String, String?>>,
|
||||
private val convertersContainer: AccountConverterFactoryContainer,
|
||||
private val resources: Resources,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : AccountsCRUDRepository {
|
||||
|
||||
|
|
@ -90,19 +98,49 @@ internal class DefaultAccountsCRUDRepository(
|
|||
}
|
||||
|
||||
override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) {
|
||||
val response = withContext(dispatchers.io) {
|
||||
tangemTechApi.getWalletArchivedAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = getETag(userWalletId),
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
val eTag = archivedAccountsETagStore.getSyncOrNull()?.get(key = userWalletId.stringValue)
|
||||
val store = getArchivedAccountsStore(userWalletId = userWalletId)
|
||||
val converter = ArchivedAccountConverter(userWalletId = userWalletId)
|
||||
|
||||
val archivedAccounts = converter.convertList(input = response.accounts)
|
||||
val response = safeApiCall(
|
||||
call = {
|
||||
val apiResponse = withContext(dispatchers.io) {
|
||||
tangemTechApi.getWalletArchivedAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
)
|
||||
}
|
||||
|
||||
store.store(value = archivedAccounts)
|
||||
saveETag(userWalletId, apiResponse)
|
||||
|
||||
apiResponse.bind()
|
||||
},
|
||||
onError = {
|
||||
if (it is HttpException && it.code == HttpException.Code.NOT_MODIFIED) {
|
||||
null
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (response != null) {
|
||||
val converter = ArchivedAccountConverter(userWalletId = userWalletId)
|
||||
|
||||
val archivedAccounts = converter.convertList(input = response.accounts)
|
||||
|
||||
store.store(value = archivedAccounts)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveAccountsLocally(accountList: AccountList) {
|
||||
val converter = convertersContainer.createWalletAccountsResponseConverter(
|
||||
userWalletId = accountList.userWalletId,
|
||||
)
|
||||
|
||||
walletAccountsSaver.store(
|
||||
userWalletId = accountList.userWalletId,
|
||||
response = converter.convert(accountList),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun saveAccounts(accountList: AccountList) {
|
||||
|
|
@ -154,9 +192,17 @@ internal class DefaultAccountsCRUDRepository(
|
|||
return accountListResponse.wallet.totalAccounts.toOption()
|
||||
}
|
||||
|
||||
override fun getTotalAccountsCount(userWalletId: UserWalletId): Flow<Option<Int>> {
|
||||
override suspend fun getTotalActiveAccountsCountSync(userWalletId: UserWalletId): Option<Int> = option {
|
||||
val accountListResponse = getAccountsResponseSync(userWalletId = userWalletId)
|
||||
|
||||
ensureNotNull(accountListResponse)
|
||||
|
||||
return accountListResponse.accounts.size.toOption()
|
||||
}
|
||||
|
||||
override fun getTotalActiveAccountsCount(userWalletId: UserWalletId): Flow<Option<Int>> {
|
||||
return getAccountsResponseStore(userWalletId = userWalletId).data
|
||||
.map { it?.wallet?.totalAccounts.toOption() }
|
||||
.map { it?.accounts?.size.toOption() }
|
||||
}
|
||||
|
||||
override fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
|
|
@ -167,8 +213,25 @@ internal class DefaultAccountsCRUDRepository(
|
|||
|
||||
override fun getUserWalletsSync(): List<UserWallet> = userWalletsStore.userWalletsSync
|
||||
|
||||
private suspend fun getETag(userWalletId: UserWalletId): String? {
|
||||
return eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
override fun checkDefaultAccountName(accountList: AccountList, accountName: AccountName) {
|
||||
val hasDefaultName = accountList.accounts.any { it.accountName is AccountName.DefaultMain }
|
||||
|
||||
if (!hasDefaultName) return
|
||||
|
||||
val defaultName = resources.getStringSafe(AccountNameUM.DefaultMain.stringResId)
|
||||
.let(AccountName::invoke).getOrNull()
|
||||
|
||||
require(defaultName != accountName) {
|
||||
"Cannot use default account name \"$accountName\" for custom accounts"
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveETag(userWalletId: UserWalletId, apiResponse: ApiResponse<*>) {
|
||||
val eTag = apiResponse.headers[ETAG_HEADER]?.firstOrNull()
|
||||
|
||||
archivedAccountsETagStore.update {
|
||||
it + (userWalletId.stringValue to eTag)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAccountsResponseSync(userWalletId: UserWalletId): GetWalletAccountsResponse? {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.datasource.utils.getSyncOrNull
|
||||
import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
|
|
@ -78,6 +78,8 @@ internal class DefaultMainAccountTokensMigration(
|
|||
},
|
||||
)
|
||||
|
||||
store.updateData { updatedResponse }
|
||||
|
||||
userTokensSaver.push(
|
||||
userWalletId = userWalletId,
|
||||
response = updatedResponse.toUserTokensResponse(),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
package com.tangem.data.account.utils
|
||||
|
||||
import com.tangem.data.account.converter.CryptoPortfolioConverter
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
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.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import javax.inject.Inject
|
||||
|
|
@ -16,22 +18,22 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Factory to create default [GetWalletAccountsResponse].
|
||||
*
|
||||
* @property userWalletsListRepository repository to get user wallet information
|
||||
* @property userWalletsStore store to get user wallet information
|
||||
* @property cryptoPortfolioCF converter factory to convert crypto portfolio accounts
|
||||
* @property userTokensResponseFactory factory to create [UserTokensResponse]
|
||||
* @property cardCryptoCurrencyFactory factory to get default coins for multi-currency wallet
|
||||
* @property networkFactory factory to create network derivation path
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultWalletAccountsResponseFactory @Inject constructor(
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory,
|
||||
private val userTokensResponseFactory: UserTokensResponseFactory,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
private val networkFactory: NetworkFactory,
|
||||
) {
|
||||
|
||||
suspend fun create(userWalletId: UserWalletId, userTokensResponse: UserTokensResponse?): GetWalletAccountsResponse {
|
||||
val userWallet = userWalletsListRepository.userWalletsSync().firstOrNull { it.walletId == userWalletId }
|
||||
fun create(userWalletId: UserWalletId, userTokensResponse: UserTokensResponse?): GetWalletAccountsResponse {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId)
|
||||
|
||||
val accountDTOs = userWallet?.let(::createDefaultAccountDTOs).orEmpty()
|
||||
val response = userTokensResponse.orDefault(userWallet = userWallet)
|
||||
|
|
@ -59,10 +61,12 @@ internal class DefaultWalletAccountsResponseFactory @Inject constructor(
|
|||
private fun UserTokensResponse?.orDefault(userWallet: UserWallet?): UserTokensResponse {
|
||||
if (this != null) return this
|
||||
|
||||
return userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = userWallet?.let(cardCryptoCurrencyFactory::createDefaultCoinsForMultiCurrencyWallet).orEmpty(),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
return userTokensResponseFactory.createDefaultResponse(
|
||||
userWallet = userWallet,
|
||||
networkFactory = networkFactory,
|
||||
accountId = userWallet?.let {
|
||||
AccountId.forCryptoPortfolio(userWalletId = it.walletId, derivationIndex = DerivationIndex.Main)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,20 +6,6 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp
|
|||
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]
|
||||
*
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ internal fun createAccountList(
|
|||
): AccountList {
|
||||
return AccountList(
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(createCryptoPortfolio(userWalletId)),
|
||||
accounts = listOf(createCryptoPortfolio(userWalletId)),
|
||||
totalAccounts = 1,
|
||||
sortType = sortType,
|
||||
groupType = groupType,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package com.tangem.data.account.converter
|
|||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import org.junit.jupiter.api.Test
|
||||
|
|
@ -16,13 +14,7 @@ class SaveWalletAccountsResponseConverterTest {
|
|||
fun convert() {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId("011")
|
||||
|
||||
val accountList = AccountList(
|
||||
userWalletId = userWalletId,
|
||||
accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)),
|
||||
totalAccounts = 1,
|
||||
)
|
||||
.getOrNull()!!
|
||||
val accountList = createAccountList(userWalletId)
|
||||
|
||||
// Act
|
||||
val actual = SaveWalletAccountsResponseConverter.convert(value = accountList)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ class DefaultSingleAccountListFetcherTest {
|
|||
// Arrange
|
||||
val params = SingleAccountListFetcher.Params(userWalletId = userWalletId)
|
||||
|
||||
coEvery { walletAccountsFetcher.fetch(userWalletId) } returns mockk()
|
||||
|
||||
// Act
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
@Test
|
||||
fun `fetch should call error handler when getWalletAccounts returns error`() = runTest {
|
||||
// Arrange
|
||||
val savedAccountsResponse = null
|
||||
val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
|
||||
val apiError = ApiResponse.Error(ApiResponseError.NetworkException())
|
||||
|
||||
accountsResponseStoreFlow.value = savedAccountsResponse
|
||||
|
|
@ -212,7 +212,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
fetchWalletAccountsErrorHandler.handle(
|
||||
error = apiError.cause,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = null,
|
||||
savedAccountsResponse = savedAccountsResponse,
|
||||
pushWalletAccounts = any(),
|
||||
storeWalletAccounts = any(),
|
||||
)
|
||||
|
|
@ -231,7 +231,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
fetchWalletAccountsErrorHandler.handle(
|
||||
error = apiError.cause,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = null,
|
||||
savedAccountsResponse = savedAccountsResponse,
|
||||
pushWalletAccounts = any(),
|
||||
storeWalletAccounts = any(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,15 +3,21 @@ package com.tangem.data.account.fetcher
|
|||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.OnlyWalletIdBody
|
||||
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.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
|
|
@ -27,14 +33,19 @@ import org.junit.jupiter.api.TestInstance
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class FetchWalletAccountsErrorHandlerTest {
|
||||
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
|
||||
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
|
||||
private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true)
|
||||
|
||||
private val handler = FetchWalletAccountsErrorHandler(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userTokensSaver = userTokensSaver,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
|
||||
eTagsStore = eTagsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse =
|
||||
|
|
@ -53,6 +64,8 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
@Test
|
||||
fun `does not update accounts when response is up to date`() = runTest {
|
||||
// Arrange
|
||||
val response = createGetWalletAccountsResponse(userWalletId)
|
||||
|
||||
val error = ApiResponseError.HttpException(
|
||||
code = Code.NOT_MODIFIED,
|
||||
message = "Not Modified",
|
||||
|
|
@ -63,7 +76,7 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
handler.handle(
|
||||
error = error,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = null,
|
||||
savedAccountsResponse = response,
|
||||
pushWalletAccounts = pushWalletAccounts,
|
||||
storeWalletAccounts = storeWalletAccounts,
|
||||
)
|
||||
|
|
@ -91,6 +104,16 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
|
||||
val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
|
||||
|
||||
val eTagValue = "etag-value"
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = Unit,
|
||||
headers = mapOf(ETAG_HEADER to listOf(eTagValue)),
|
||||
code = Code.CREATED,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.createWallet(OnlyWalletIdBody(userWalletId.stringValue))
|
||||
} returns apiResponse
|
||||
coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse
|
||||
|
||||
// Act
|
||||
|
|
@ -105,6 +128,8 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
// Assert
|
||||
coVerify {
|
||||
userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
|
||||
tangemTechApi.createWallet(OnlyWalletIdBody(userWalletId.stringValue))
|
||||
eTagsStore.store(userWalletId, ETagsStore.Key.WalletAccounts, eTagValue)
|
||||
pushWalletAccounts(userWalletId, listOf(accountDTO))
|
||||
storeWalletAccounts(userWalletId, savedAccountsResponse)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.account.repository
|
||||
|
||||
import android.content.res.Resources
|
||||
import arrow.core.None
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
|
|
@ -10,7 +11,6 @@ import com.tangem.data.account.store.AccountsResponseStoreFactory
|
|||
import com.tangem.data.account.store.ArchivedAccountsStore
|
||||
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
|
||||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
|
|
@ -54,12 +54,14 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val userTokensSaver: UserTokensSaver = mockk()
|
||||
private val eTagsStore: ETagsStore = mockk()
|
||||
private val archivedAccountsETagStore: RuntimeStateStore<Map<String, String?>> = mockk(relaxUnitFun = true)
|
||||
|
||||
private val convertersContainer: AccountConverterFactoryContainer = mockk()
|
||||
private val accountListConverter: AccountListConverter = mockk()
|
||||
private val cryptoPortfolioConverter: CryptoPortfolioConverter = mockk()
|
||||
|
||||
private val resources: Resources = mockk()
|
||||
|
||||
private val repository = DefaultAccountsCRUDRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
walletAccountsSaver = walletAccountsSaver,
|
||||
|
|
@ -67,8 +69,9 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
archivedAccountsStoreFactory = archivedAccountsStoreFactory,
|
||||
userWalletsStore = userWalletsStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
eTagsStore = eTagsStore,
|
||||
archivedAccountsETagStore = archivedAccountsETagStore,
|
||||
convertersContainer = convertersContainer,
|
||||
resources = resources,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
|
|
@ -532,7 +535,7 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
|
||||
val archivedAccount = ArchivedAccountConverter(userWalletId).convert(accountDTO)
|
||||
|
||||
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
|
||||
coEvery { archivedAccountsETagStore.getSyncOrNull() } returns mapOf(userWalletId.stringValue to eTag)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
|
||||
|
|
@ -546,9 +549,10 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
Truth.assertThat(actual).containsExactly(archivedAccount)
|
||||
|
||||
coVerifyOrder {
|
||||
eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts)
|
||||
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
|
||||
archivedAccountsETagStore.getSyncOrNull()
|
||||
archivedAccountsStoreFactory.create(userWalletId)
|
||||
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
|
||||
archivedAccountsETagStore.update(any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -558,7 +562,7 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
val eTag = "etag123"
|
||||
val exception = Exception("API error")
|
||||
|
||||
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
|
||||
coEvery { archivedAccountsETagStore.getSyncOrNull() } returns mapOf(userWalletId.stringValue to eTag)
|
||||
coEvery { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag) } throws exception
|
||||
|
||||
// Act
|
||||
|
|
@ -570,7 +574,7 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
Truth.assertThat(archivedAccountsStore.getSyncOrNull()).isNull()
|
||||
|
||||
coVerifyOrder {
|
||||
eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts)
|
||||
archivedAccountsETagStore.getSyncOrNull()
|
||||
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
|
||||
}
|
||||
}
|
||||
|
|
@ -644,15 +648,15 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetTotalAccountsCountSync {
|
||||
inner class GetTotalActiveAccountsCountSync {
|
||||
|
||||
@Test
|
||||
fun `getTotalAccountsCountSync returns None if account list response is null`() = runTest {
|
||||
fun `getTotalActiveAccountsCountSync returns None if account list response is null`() = runTest {
|
||||
// Arrange
|
||||
accountsResponseStoreFlow.value = null
|
||||
|
||||
// Act
|
||||
val actual = repository.getTotalAccountsCountSync(userWalletId)
|
||||
val actual = repository.getTotalActiveAccountsCountSync(userWalletId)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(None)
|
||||
|
|
@ -664,20 +668,22 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `getTotalAccountsCountSync returns Some with totalAccounts when response is valid`() = runTest {
|
||||
fun `getTotalActiveAccountsCountSync returns Some with totalAccounts when response is valid`() = runTest {
|
||||
// Arrange
|
||||
val totalAccounts = 5
|
||||
val response = mockk<GetWalletAccountsResponse> {
|
||||
every { this@mockk.wallet.totalAccounts } returns totalAccounts
|
||||
val response = createGetWalletAccountsResponse(userWalletId).let {
|
||||
it.copy(
|
||||
wallet = it.wallet.copy(totalAccounts = totalAccounts),
|
||||
)
|
||||
}
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
// Act
|
||||
val actual = repository.getTotalAccountsCountSync(userWalletId)
|
||||
val actual = repository.getTotalActiveAccountsCountSync(userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = totalAccounts.toOption()
|
||||
val expected = 1.toOption()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
verifyOrder {
|
||||
|
|
@ -689,15 +695,15 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetTotalAccountsCount {
|
||||
inner class GetTotalActiveAccountsCount {
|
||||
|
||||
@Test
|
||||
fun `getTotalAccountsCount emits 0 when account list response is null`() = runTest {
|
||||
fun `getTotalActiveAccountsCount emits 0 when account list response is null`() = runTest {
|
||||
// Arrange
|
||||
accountsResponseStoreFlow.value = null
|
||||
|
||||
// Act
|
||||
val flow = repository.getTotalAccountsCount(userWalletId)
|
||||
val flow = repository.getTotalActiveAccountsCount(userWalletId)
|
||||
val actual = getEmittedValues(flow)
|
||||
|
||||
// Assert
|
||||
|
|
@ -710,21 +716,23 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `getTotalAccountsCount emits correct value when response is valid`() = runTest {
|
||||
fun `getTotalActiveAccountsCount emits correct value when response is valid`() = runTest {
|
||||
// Arrange
|
||||
val totalAccounts = 7
|
||||
val response = mockk<GetWalletAccountsResponse> {
|
||||
every { this@mockk.wallet.totalAccounts } returns totalAccounts
|
||||
val response = createGetWalletAccountsResponse(userWalletId).let {
|
||||
it.copy(
|
||||
wallet = it.wallet.copy(totalAccounts = totalAccounts),
|
||||
)
|
||||
}
|
||||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
// Act
|
||||
val flow = repository.getTotalAccountsCount(userWalletId)
|
||||
val flow = repository.getTotalActiveAccountsCount(userWalletId)
|
||||
val actual = getEmittedValues(flow)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly(totalAccounts.toOption())
|
||||
Truth.assertThat(actual).containsExactly(1.toOption())
|
||||
verifyOrder {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import com.tangem.data.account.converter.createWalletAccountDTO
|
|||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -183,6 +183,8 @@ class DefaultMainAccountTokensMigrationTest {
|
|||
|
||||
accountsResponseStoreFlow.value = response
|
||||
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns mockk()
|
||||
|
||||
// Act
|
||||
val actual = migration.migrate(userWalletId, derivationIndex)
|
||||
|
||||
|
|
@ -199,6 +201,7 @@ class DefaultMainAccountTokensMigrationTest {
|
|||
coVerifySequence {
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.data
|
||||
accountsResponseStore.updateData(any())
|
||||
userTokensSaver.push(
|
||||
userWalletId = userWalletId,
|
||||
response = migratedResponse.toUserTokensResponse(),
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package com.tangem.data.account.utils
|
|||
import com.google.common.truth.Truth
|
||||
import com.tangem.data.account.converter.CryptoPortfolioConverter
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
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.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -23,17 +23,17 @@ import org.junit.jupiter.api.TestInstance
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultWalletAccountsResponseFactoryTest {
|
||||
|
||||
private val userWalletsListRepository = mockk<UserWalletsListRepository>()
|
||||
private val userWalletsStore = mockk<UserWalletsStore>()
|
||||
private val cryptoPortfolioCF = mockk<CryptoPortfolioConverter.Factory>()
|
||||
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
|
||||
private val userTokensResponseFactory = mockk<UserTokensResponseFactory>()
|
||||
private val cardCryptoCurrencyFactory = mockk<CardCryptoCurrencyFactory>()
|
||||
private val networkFactory = mockk<NetworkFactory>()
|
||||
|
||||
private val factory = DefaultWalletAccountsResponseFactory(
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
userWalletsStore = userWalletsStore,
|
||||
cryptoPortfolioCF = cryptoPortfolioCF,
|
||||
userTokensResponseFactory = userTokensResponseFactory,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
networkFactory = networkFactory,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
|
@ -46,11 +46,11 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(
|
||||
userWalletsListRepository,
|
||||
userWalletsStore,
|
||||
cryptoPortfolioCF,
|
||||
cryptoPortfolioConverter,
|
||||
userTokensResponseFactory,
|
||||
cardCryptoCurrencyFactory,
|
||||
networkFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -63,12 +63,12 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns emptyList()
|
||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns null
|
||||
every {
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = emptyList(),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
userTokensResponseFactory.createDefaultResponse(
|
||||
userWallet = null,
|
||||
networkFactory = networkFactory,
|
||||
accountId = null,
|
||||
)
|
||||
} returns userTokensResponse
|
||||
|
||||
|
|
@ -88,11 +88,11 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsListRepository.userWalletsSync()
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = emptyList(),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
userWalletsStore.getSyncOrNull(userWalletId)
|
||||
userTokensResponseFactory.createDefaultResponse(
|
||||
userWallet = null,
|
||||
networkFactory = networkFactory,
|
||||
accountId = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -104,27 +104,24 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
every { walletId } returns userWalletId
|
||||
}
|
||||
|
||||
val defaultCoins = listOf(mockk<CryptoCurrency.Coin>())
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
|
||||
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns defaultCoins
|
||||
val accounts = AccountList.empty(userWallet.walletId).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
|
||||
val defaultResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
tokens = listOf(mockk(relaxed = true)),
|
||||
)
|
||||
|
||||
every {
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = defaultCoins,
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
userTokensResponseFactory.createDefaultResponse(
|
||||
userWallet = userWallet,
|
||||
networkFactory = networkFactory,
|
||||
accountId = accounts.first().accountId,
|
||||
)
|
||||
} returns defaultResponse
|
||||
|
||||
val accounts = AccountList.empty(userWallet.walletId).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
||||
val accountsDTO = createWalletAccountDTO(userWalletId)
|
||||
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO)
|
||||
|
||||
|
|
@ -145,13 +142,12 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsListRepository.userWalletsSync()
|
||||
userWalletsStore.getSyncOrNull(userWalletId)
|
||||
cryptoPortfolioConverter.convertListBack(accounts)
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet)
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = defaultCoins,
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
userTokensResponseFactory.createDefaultResponse(
|
||||
userWallet = userWallet,
|
||||
networkFactory = networkFactory,
|
||||
accountId = accounts.first().accountId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -162,22 +158,26 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
val userWallet = mockk<UserWallet>(relaxed = true) {
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
|
||||
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList()
|
||||
|
||||
val accounts = AccountList.empty(userWallet.walletId).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
|
||||
val defaultResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
every {
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = emptyList(),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
userTokensResponseFactory.createDefaultResponse(
|
||||
userWallet = userWallet,
|
||||
networkFactory = networkFactory,
|
||||
accountId = accounts.first().accountId,
|
||||
)
|
||||
} returns defaultResponse
|
||||
val accounts = AccountList.empty(userWallet.walletId).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
||||
every { cryptoPortfolioConverter.convertListBack(accounts) } returns emptyList()
|
||||
|
||||
// Act
|
||||
|
|
@ -203,7 +203,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
every { walletId } returns userWalletId
|
||||
}
|
||||
val assignedTokens = listOf(mockk<CryptoCurrency.Token>(), mockk<CryptoCurrency.Token>())
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
|
||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
val userTokensResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ 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.datasource.api.tangemTech.models.account.flattenTokens
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.common.account
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
|
|
@ -11,5 +12,8 @@ interface WalletAccountsFetcher {
|
|||
|
||||
/** Fetch wallet accounts by [userWalletId] */
|
||||
@Throws
|
||||
suspend fun fetch(userWalletId: UserWalletId)
|
||||
suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse
|
||||
|
||||
/** Get saved wallet accounts by [userWalletId] */
|
||||
suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse?
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.blockchainsdk.utils.fromNetworkId
|
|||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -18,7 +19,7 @@ class CryptoCurrencyFactory(
|
|||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
) {
|
||||
|
||||
private val networkFactory by lazy(LazyThreadSafetyMode.NONE) { NetworkFactory(excludedBlockchains) }
|
||||
val networkFactory by lazy(LazyThreadSafetyMode.NONE) { NetworkFactory(excludedBlockchains) }
|
||||
|
||||
@Suppress("LongParameterList") // Yep, it's long
|
||||
fun createToken(
|
||||
|
|
@ -48,6 +49,7 @@ class CryptoCurrencyFactory(
|
|||
blockchain: Blockchain,
|
||||
extraDerivationPath: String?,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): CryptoCurrency.Token? {
|
||||
if (blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
|
||||
|
|
@ -58,6 +60,7 @@ class CryptoCurrencyFactory(
|
|||
blockchain = blockchain,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
) ?: return null
|
||||
|
||||
val id = getTokenId(network, sdkToken)
|
||||
|
|
@ -74,11 +77,16 @@ class CryptoCurrencyFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun createCoin(chainId: Int, extraDerivationPath: String?, userWallet: UserWallet): CryptoCurrency.Coin? {
|
||||
fun createCoin(
|
||||
chainId: Int,
|
||||
extraDerivationPath: String?,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): CryptoCurrency.Coin? {
|
||||
val blockchain: Blockchain? = Chain.entries.find { it.id == chainId }?.blockchain
|
||||
|
||||
return if (blockchain != null) {
|
||||
createCoin(blockchain, extraDerivationPath, userWallet)
|
||||
createCoin(blockchain, extraDerivationPath, userWallet, accountIndex)
|
||||
} else {
|
||||
Timber.e("Unable to get blockchain from chainId == $chainId")
|
||||
null
|
||||
|
|
@ -89,6 +97,7 @@ class CryptoCurrencyFactory(
|
|||
blockchain: Blockchain,
|
||||
extraDerivationPath: String?,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): CryptoCurrency.Coin? {
|
||||
if (blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
|
||||
|
|
@ -99,6 +108,7 @@ class CryptoCurrencyFactory(
|
|||
blockchain = blockchain,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
) ?: return null
|
||||
|
||||
return createCoin(network)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
package com.tangem.data.common.currency
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.tokens.getDefaultWalletBlockchains
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -23,10 +25,13 @@ import com.tangem.domain.models.wallet.isMultiCurrency
|
|||
* @property userWalletsStore user wallets store
|
||||
* @property userTokensResponseStore user tokens response store
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultCardCryptoCurrencyFactory(
|
||||
private val demoConfig: DemoConfig,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
) : CardCryptoCurrencyFactory {
|
||||
|
|
@ -99,25 +104,7 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
override fun createDefaultCoinsForMultiCurrencyWallet(userWallet: UserWallet): List<CryptoCurrency.Coin> {
|
||||
require(userWallet.isMultiCurrency) { "It isn't multi-currency wallet" }
|
||||
|
||||
val blockchains = when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
val card = userWallet.scanResponse.card
|
||||
|
||||
var blockchainsInternal = if (demoConfig.isDemoCardId(card.cardId)) {
|
||||
demoConfig.demoBlockchains
|
||||
} else {
|
||||
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
|
||||
}
|
||||
|
||||
if (card.isTestCard) {
|
||||
blockchainsInternal = blockchainsInternal.mapNotNull { it.getTestnetVersion() }
|
||||
}
|
||||
|
||||
blockchainsInternal
|
||||
}
|
||||
|
||||
is UserWallet.Hot -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
|
||||
}
|
||||
val blockchains = getDefaultWalletBlockchains(userWallet, demoConfig)
|
||||
|
||||
return blockchains.mapNotNull {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
|
|
@ -152,15 +139,32 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
userWallet: UserWallet,
|
||||
networks: Set<Network>,
|
||||
): Map<Network, List<CryptoCurrency>> {
|
||||
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
|
||||
?: return emptyMap()
|
||||
val existingNetworkWithCurrencies = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
|
||||
?: return emptyMap()
|
||||
|
||||
val existingNetworkWithCurrencies = responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = response.tokens.filter { token ->
|
||||
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
|
||||
},
|
||||
userWallet = userWallet,
|
||||
)
|
||||
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = accountDTO.tokens.orEmpty().filter { token ->
|
||||
networks.any {
|
||||
it.backendId == token.networkId && it.derivationPath.value == token.derivationPath
|
||||
}
|
||||
},
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
|
||||
?: return emptyMap()
|
||||
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = response.tokens.filter { token ->
|
||||
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
|
||||
},
|
||||
userWallet = userWallet,
|
||||
)
|
||||
}
|
||||
.groupBy(CryptoCurrency::network)
|
||||
|
||||
return networks.associateWith { emptyList<CryptoCurrency>() } + existingNetworkWithCurrencies
|
||||
|
|
@ -170,15 +174,28 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
userWallet: UserWallet,
|
||||
rawIds: Set<Network.RawID>,
|
||||
): Map<Network.RawID, List<CryptoCurrency>> {
|
||||
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
|
||||
?: return emptyMap()
|
||||
|
||||
val networkIds = rawIds.map { it.toBlockchain().toNetworkId() }
|
||||
|
||||
return responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = response.tokens.filter { token -> token.networkId in networkIds },
|
||||
userWallet = userWallet,
|
||||
)
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
|
||||
?: return emptyMap()
|
||||
|
||||
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds },
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
|
||||
?: return emptyMap()
|
||||
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = response.tokens.filter { token -> token.networkId in networkIds },
|
||||
userWallet = userWallet,
|
||||
)
|
||||
}
|
||||
.groupBy { it.network.id.rawId }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.blockchainsdk.utils.toCoinId
|
|||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -18,26 +19,31 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
private val networkFactory: NetworkFactory,
|
||||
) {
|
||||
|
||||
fun createCurrency(currencyId: String, response: UserTokensResponse, userWallet: UserWallet): CryptoCurrency {
|
||||
return response.tokens
|
||||
.asSequence()
|
||||
.mapNotNull { createCurrency(it, userWallet) }
|
||||
.first { it.id.value == currencyId }
|
||||
fun createCurrencies(
|
||||
response: UserTokensResponse,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): List<CryptoCurrency> {
|
||||
return createCurrencies(tokens = response.tokens, userWallet = userWallet, accountIndex = accountIndex)
|
||||
}
|
||||
|
||||
fun createCurrencies(response: UserTokensResponse, userWallet: UserWallet): List<CryptoCurrency> {
|
||||
return createCurrencies(tokens = response.tokens, userWallet = userWallet)
|
||||
}
|
||||
|
||||
fun createCurrencies(tokens: List<UserTokensResponse.Token>, userWallet: UserWallet): List<CryptoCurrency> {
|
||||
fun createCurrencies(
|
||||
tokens: List<UserTokensResponse.Token>,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): List<CryptoCurrency> {
|
||||
return tokens
|
||||
.asSequence()
|
||||
.mapNotNull { createCurrency(it, userWallet) }
|
||||
.mapNotNull { createCurrency(it, userWallet, accountIndex) }
|
||||
.distinctBy(CryptoCurrency::id)
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun createCurrency(responseToken: UserTokensResponse.Token, userWallet: UserWallet): CryptoCurrency? {
|
||||
fun createCurrency(
|
||||
responseToken: UserTokensResponse.Token,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): CryptoCurrency? {
|
||||
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")
|
||||
|
|
@ -52,6 +58,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
|
|||
blockchain = blockchain,
|
||||
extraDerivationPath = responseToken.derivationPath,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
) ?: return null
|
||||
|
||||
return createCurrency(responseToken = responseToken, userWallet = userWallet, network = network)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
package com.tangem.data.common.currency
|
||||
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.common.tokens.getDefaultWalletBlockchains
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO: [REDACTED_JIRA]
|
||||
class UserTokensResponseFactory @Inject constructor() {
|
||||
|
||||
fun createUserTokensResponse(
|
||||
|
|
@ -43,4 +49,38 @@ class UserTokensResponseFactory @Inject constructor() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createDefaultResponse(
|
||||
userWallet: UserWallet?,
|
||||
networkFactory: NetworkFactory,
|
||||
accountId: AccountId?,
|
||||
): UserTokensResponse {
|
||||
val tokens = userWallet?.let {
|
||||
getDefaultWalletBlockchains(userWallet = it, demoConfig = DemoConfig())
|
||||
.map { blockchain ->
|
||||
val derivationPath = networkFactory.createDerivationPath(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
cardDerivationStyleProvider = userWallet.derivationStyleProvider,
|
||||
).value
|
||||
|
||||
UserTokensResponse.Token(
|
||||
id = blockchain.toCoinId(),
|
||||
accountId = accountId?.value,
|
||||
networkId = blockchain.toNetworkId(),
|
||||
derivationPath = derivationPath,
|
||||
name = blockchain.getCoinName(),
|
||||
symbol = blockchain.currency,
|
||||
decimals = blockchain.decimals(),
|
||||
contractAddress = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
tokens = tokens.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.common.di
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.cache.etag.DefaultETagsStore
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.*
|
||||
|
|
@ -30,6 +31,8 @@ internal object DataCommonModule {
|
|||
fun provideCardCryptoCurrencyFactory(
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
walletAccountsFetcher: WalletAccountsFetcher,
|
||||
userTokensResponseStore: UserTokensResponseStore,
|
||||
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
): CardCryptoCurrencyFactory {
|
||||
|
|
@ -37,6 +40,8 @@ internal object DataCommonModule {
|
|||
demoConfig = DemoConfig(),
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
userWalletsStore = userWalletsStore,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
walletAccountsFetcher = walletAccountsFetcher,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
|||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.domain.card.common.extensions.canHandleToken
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.lib.crypto.derivation.toMutable
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -21,6 +23,7 @@ import javax.inject.Inject
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LargeClass")
|
||||
class NetworkFactory @Inject constructor(
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
) {
|
||||
|
|
@ -32,13 +35,19 @@ class NetworkFactory @Inject constructor(
|
|||
* @param extraDerivationPath extra derivation path
|
||||
* @param userWallet user wallet
|
||||
*/
|
||||
fun create(blockchain: Blockchain, extraDerivationPath: String?, userWallet: UserWallet): Network? {
|
||||
fun create(
|
||||
blockchain: Blockchain,
|
||||
extraDerivationPath: String?,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): Network? {
|
||||
return create(
|
||||
blockchain = blockchain,
|
||||
derivationPath = createDerivationPath(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
cardDerivationStyleProvider = userWallet.derivationStyleProvider,
|
||||
accountIndex = accountIndex,
|
||||
),
|
||||
canHandleTokens = userWallet.canHandleToken(
|
||||
blockchain = blockchain,
|
||||
|
|
@ -98,6 +107,7 @@ class NetworkFactory @Inject constructor(
|
|||
extraDerivationPath: String?,
|
||||
derivationStyleProvider: DerivationStyleProvider?,
|
||||
canHandleTokens: Boolean,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): Network? {
|
||||
return create(
|
||||
blockchain = blockchain,
|
||||
|
|
@ -105,6 +115,7 @@ class NetworkFactory @Inject constructor(
|
|||
blockchain = blockchain,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
cardDerivationStyleProvider = derivationStyleProvider,
|
||||
accountIndex = accountIndex,
|
||||
),
|
||||
canHandleTokens = canHandleTokens,
|
||||
)
|
||||
|
|
@ -148,14 +159,15 @@ class NetworkFactory @Inject constructor(
|
|||
return true
|
||||
}
|
||||
|
||||
private fun createDerivationPath(
|
||||
fun createDerivationPath(
|
||||
blockchain: Blockchain,
|
||||
extraDerivationPath: String?,
|
||||
cardDerivationStyleProvider: DerivationStyleProvider?,
|
||||
accountIndex: DerivationIndex? = null,
|
||||
): Network.DerivationPath {
|
||||
if (cardDerivationStyleProvider == null) return Network.DerivationPath.None
|
||||
|
||||
val defaultDerivationPath = getDefaultDerivationPath(blockchain, cardDerivationStyleProvider)
|
||||
val defaultDerivationPath = getDefaultDerivationPath(blockchain, cardDerivationStyleProvider, accountIndex)
|
||||
|
||||
return if (extraDerivationPath.isNullOrBlank()) {
|
||||
if (defaultDerivationPath.isNullOrBlank()) {
|
||||
|
|
@ -164,10 +176,11 @@ class NetworkFactory @Inject constructor(
|
|||
Network.DerivationPath.Card(defaultDerivationPath)
|
||||
}
|
||||
} else {
|
||||
if (extraDerivationPath == defaultDerivationPath) {
|
||||
Network.DerivationPath.Card(defaultDerivationPath)
|
||||
} else {
|
||||
val isMainIndexOrNull = accountIndex == null || accountIndex == DerivationIndex.Main
|
||||
if (extraDerivationPath != defaultDerivationPath && isMainIndexOrNull) {
|
||||
Network.DerivationPath.Custom(extraDerivationPath)
|
||||
} else {
|
||||
Network.DerivationPath.Card(extraDerivationPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,8 +188,19 @@ class NetworkFactory @Inject constructor(
|
|||
private fun getDefaultDerivationPath(
|
||||
blockchain: Blockchain,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
accountIndex: DerivationIndex?,
|
||||
): String? {
|
||||
return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath
|
||||
val default = blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())
|
||||
?: return null
|
||||
|
||||
return if (accountIndex == null || accountIndex == DerivationIndex.Main) {
|
||||
default
|
||||
} else {
|
||||
default.toMutable()
|
||||
.replaceAccountNode(value = accountIndex.value.toLong(), blockchain = blockchain)
|
||||
.apply()
|
||||
}
|
||||
.rawPath
|
||||
}
|
||||
|
||||
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
|
||||
|
|
@ -344,8 +368,8 @@ class NetworkFactory @Inject constructor(
|
|||
Blockchain.Pepecoin, Blockchain.PepecoinTestnet,
|
||||
Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet,
|
||||
Blockchain.Quai, Blockchain.QuaiTestnet,
|
||||
// Blockchain.Linea, Blockchain.LineaTestnet,
|
||||
// Blockchain.ArbitrumNova,
|
||||
Blockchain.Linea, Blockchain.LineaTestnet,
|
||||
Blockchain.ArbitrumNova,
|
||||
-> Network.TransactionExtrasType.NONE
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.data.common.tokens
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
/**
|
||||
* Returns the default blockchains for the multi-currency wallet.
|
||||
*
|
||||
* @param userWallet The user's wallet, which can be either a cold or hot wallet.
|
||||
* @param demoConfig Configuration for demo cards, which may specify different default blockchains.
|
||||
*/
|
||||
fun getDefaultWalletBlockchains(userWallet: UserWallet, demoConfig: DemoConfig): Collection<Blockchain> {
|
||||
return when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
val card = userWallet.scanResponse.card
|
||||
|
||||
var blockchainsInternal = if (demoConfig.isDemoCardId(card.cardId)) {
|
||||
demoConfig.demoBlockchains
|
||||
} else {
|
||||
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
|
||||
}
|
||||
|
||||
if (card.isTestCard) {
|
||||
blockchainsInternal = blockchainsInternal.mapNotNull { it.getTestnetVersion() }
|
||||
}
|
||||
|
||||
blockchainsInternal
|
||||
}
|
||||
is UserWallet.Hot -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,10 +9,12 @@ import com.tangem.common.test.domain.card.MockScanResponseFactory
|
|||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
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.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.configs.GenericCardConfig
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
|
|
@ -37,6 +39,8 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
|
|||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val userTokensResponseStore: UserTokensResponseStore = mockk()
|
||||
private val excludedBlockchains = ExcludedBlockchains()
|
||||
private val accountsFeatureToggles = mockk<AccountsFeatureToggles>()
|
||||
private val walletAccountsFetcher = mockk<WalletAccountsFetcher>()
|
||||
|
||||
private val factory = DefaultCardCryptoCurrencyFactory(
|
||||
demoConfig = DemoConfig(),
|
||||
|
|
@ -46,6 +50,8 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
|
|||
responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(
|
||||
networkFactory = NetworkFactory(excludedBlockchains = excludedBlockchains),
|
||||
),
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
walletAccountsFetcher = walletAccountsFetcher,
|
||||
)
|
||||
|
||||
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
|
||||
|
|
@ -57,7 +63,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
|
|||
|
||||
@BeforeEach
|
||||
fun init() {
|
||||
clearMocks(userWalletsStore, userTokensResponseStore, iconUri)
|
||||
clearMocks(userWalletsStore, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri)
|
||||
|
||||
mockkStatic(Uri::class)
|
||||
every { Uri.parse(any()) } returns iconUri
|
||||
|
|
@ -75,6 +81,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
|
|||
val userTokensResponse = model.userTokensResponse
|
||||
val network = ethereum.network
|
||||
|
||||
every { accountsFeatureToggles.isFeatureEnabled } returns false
|
||||
coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
|
||||
|
||||
|
|
@ -236,6 +243,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
|
|||
val networks = setOf(ethereum.network, bitcoin.network)
|
||||
val userTokensResponse = model.userTokensResponse
|
||||
|
||||
every { accountsFeatureToggles.isFeatureEnabled } returns false
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
|
||||
|
||||
// Act
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
package com.tangem.data.express
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.data.express.converter.ExpressAssetConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.ExpressAssetsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<ExpressAsset>>>
|
||||
|
||||
/**
|
||||
* Default implementation of [ExpressServiceFetcher]
|
||||
*
|
||||
* @property tangemExpressApi express api
|
||||
* @property expressAssetsStore local storage
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultExpressServiceFetcher @Inject constructor(
|
||||
private val tangemExpressApi: TangemExpressApi,
|
||||
private val expressAssetsStore: ExpressAssetsStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ExpressServiceFetcher {
|
||||
|
||||
private val initializationStatuses =
|
||||
MutableStateFlow<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
|
||||
|
||||
override suspend fun fetch(userWalletId: UserWalletId, assetIds: Set<ExpressAsset.ID>): Either<Throwable, Unit> =
|
||||
either {
|
||||
val userWallet = arrow.core.raise.catch(
|
||||
block = { userWalletsStore.getSyncStrict(userWalletId) },
|
||||
catch = ::raise,
|
||||
)
|
||||
|
||||
fetch(userWallet = userWallet, assetIds = assetIds).bind()
|
||||
}
|
||||
|
||||
override suspend fun fetch(userWallet: UserWallet, assetIds: Set<ExpressAsset.ID>): Either<Throwable, Unit> {
|
||||
return Either.catchOn(dispatchers.io) {
|
||||
val initializationStatus = getInitializationStatusInternal(userWallet.walletId)
|
||||
|
||||
try {
|
||||
if (assetIds.isNotEmpty()) {
|
||||
val tokenList = assetIds.map {
|
||||
LeastTokenInfo(contractAddress = it.contractAddress, network = it.networkId)
|
||||
}
|
||||
|
||||
val response = tangemExpressApi.getAssets(
|
||||
userWalletId = userWallet.walletId.stringValue,
|
||||
refCode = getRefCode(userWallet, appPreferencesStore),
|
||||
body = AssetsRequestBody(tokensList = tokenList),
|
||||
).getOrThrow()
|
||||
|
||||
expressAssetsStore.store(userWallet.walletId, response)
|
||||
|
||||
val expressAssets = ExpressAssetConverter.convertList(response)
|
||||
initializationStatus.update { expressAssets.lceContent() }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) {
|
||||
initializationStatus.update { e.lceError() }
|
||||
}
|
||||
Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<ExpressAsset>>> {
|
||||
return flow { getInitializationStatusInternal(userWalletId).collect { emit(it) } }
|
||||
}
|
||||
|
||||
@Suppress("SuspendFunWithFlowReturnType")
|
||||
private suspend fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
|
||||
val initializationStatus = initializationStatuses.value[userWalletId]
|
||||
if (initializationStatus != null) return initializationStatus
|
||||
|
||||
val cached = expressAssetsStore.getSyncOrNull(userWalletId)?.let(ExpressAssetConverter::convertList)
|
||||
val default: InitializationStatusFlow = MutableStateFlow(value = cached?.lceContent() ?: lceLoading())
|
||||
|
||||
initializationStatuses.update { statuses ->
|
||||
statuses.toMutableMap().apply {
|
||||
put(key = userWalletId, value = default)
|
||||
}
|
||||
}
|
||||
|
||||
return default
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.data.express.converter
|
||||
|
||||
import com.tangem.datasource.api.express.models.response.Asset
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converts an [Asset] from the data layer to an [ExpressAsset] in the domain layer.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object ExpressAssetConverter : Converter<Asset, ExpressAsset> {
|
||||
|
||||
override fun convert(value: Asset): ExpressAsset {
|
||||
return ExpressAsset(
|
||||
id = ExpressAsset.ID(
|
||||
networkId = value.network,
|
||||
contractAddress = value.contractAddress,
|
||||
),
|
||||
isExchangeAvailable = value.exchangeAvailable,
|
||||
isOnrampAvailable = value.onrampAvailable,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.express.di
|
|||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.express.DefaultExpressRepository
|
||||
import com.tangem.data.express.DefaultExpressServiceFetcher
|
||||
import com.tangem.data.express.converter.ExpressErrorConverter
|
||||
import com.tangem.data.express.error.DefaultExpressErrorResolver
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
|
|
@ -10,6 +11,7 @@ import com.tangem.datasource.di.NetworkMoshi
|
|||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.express.ExpressErrorResolver
|
||||
import com.tangem.domain.express.ExpressRepository
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -43,4 +45,10 @@ internal object ExpressDataModule {
|
|||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideExpressServiceFetcher(impl: DefaultExpressServiceFetcher): ExpressServiceFetcher {
|
||||
return impl
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ android {
|
|||
dependencies {
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.manageTokens)
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ internal class DefaultCustomTokensRepository(
|
|||
"User tokens not found for user wallet [$userWalletId] while removing currency"
|
||||
}
|
||||
|
||||
val token = userTokensResponseFactory.createResponseToken(cryptoCurrency)
|
||||
val token = userTokensResponseFactory.createResponseToken(currency = cryptoCurrency, accountId = null)
|
||||
userTokensSaver.storeAndPush(
|
||||
userWalletId = userWalletId,
|
||||
response = storedCurrencies.copy(tokens = storedCurrencies.tokens.filterNot { it == token }),
|
||||
|
|
@ -249,6 +249,27 @@ internal class DefaultCustomTokensRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun convertToCryptoCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
currency: ManagedCryptoCurrency.Custom,
|
||||
): CryptoCurrency {
|
||||
return when (currency) {
|
||||
is ManagedCryptoCurrency.Custom.Coin -> createCoin(
|
||||
userWalletId = userWalletId,
|
||||
networkId = currency.network.id,
|
||||
derivationPath = currency.network.derivationPath,
|
||||
)
|
||||
is ManagedCryptoCurrency.Custom.Token -> cryptoCurrencyFactory.createToken(
|
||||
network = currency.network,
|
||||
rawId = currency.currencyId.rawCurrencyId,
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
decimals = currency.decimals,
|
||||
contractAddress = currency.contractAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> = withContext(dispatchers.io) {
|
||||
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"User wallet [$userWalletId] not found while getting supported networks"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
|||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
|
|
@ -16,20 +17,20 @@ import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
|
|||
import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
|
||||
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.card.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.card.common.extensions.canHandleBlockchain
|
||||
import com.tangem.domain.card.common.extensions.canHandleToken
|
||||
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
||||
import com.tangem.domain.card.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.card.common.extensions.supportedTokens
|
||||
import com.tangem.domain.card.common.extensions.*
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.managetokens.model.*
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
|
||||
import com.tangem.domain.managetokens.repository.ManageTokensRepository
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -40,7 +41,7 @@ import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher.Request
|
|||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultManageTokensRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
|
|
@ -51,10 +52,16 @@ internal class DefaultManageTokensRepository(
|
|||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
networkFactory: NetworkFactory,
|
||||
) : ManageTokensRepository {
|
||||
|
||||
private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory(networkFactory, excludedBlockchains)
|
||||
private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory(
|
||||
networkFactory = networkFactory,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
)
|
||||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
|
||||
// region getTokenListBatchFlow
|
||||
|
|
@ -82,7 +89,10 @@ internal class DefaultManageTokensRepository(
|
|||
val userWallet = request.params.userWalletId?.let(userWalletsStore::getSyncStrict)
|
||||
|
||||
if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.isTestCard) {
|
||||
fetchTestnetCurrencies(userWallet, request)
|
||||
when (val params = request.params) {
|
||||
is ManageTokensListConfig.Account -> fetchTestnetCurrencies(userWallet, params)
|
||||
is ManageTokensListConfig.Wallet -> fetchTestnetCurrenciesLegacy(userWallet, params)
|
||||
}
|
||||
} else {
|
||||
fetchCurrencies(
|
||||
userWallet = userWallet,
|
||||
|
|
@ -94,7 +104,6 @@ internal class DefaultManageTokensRepository(
|
|||
},
|
||||
)
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
private suspend fun fetchCurrencies(
|
||||
userWallet: UserWallet?,
|
||||
request: Request<ManageTokensListConfig>,
|
||||
|
|
@ -127,31 +136,22 @@ internal class DefaultManageTokensRepository(
|
|||
coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) },
|
||||
)
|
||||
|
||||
val tokensResponse = request.params.userWalletId?.let { userWalletId ->
|
||||
if (loadUserTokensFromRemote && userWallet != null) {
|
||||
safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) {
|
||||
// save tokens response only if loadUserTokensFromRemote is true and it means onboarding call
|
||||
createAndSaveDefaultUserTokensResponse(userWallet = userWallet)
|
||||
}
|
||||
} else {
|
||||
getSavedUserTokensResponseSync(userWalletId)
|
||||
}
|
||||
}
|
||||
val items = if (isFirstBatchFetching &&
|
||||
tokensResponse != null &&
|
||||
userWallet != null &&
|
||||
query == null
|
||||
) {
|
||||
managedCryptoCurrencyFactory.createWithCustomTokens(
|
||||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
val items = when (val params = request.params) {
|
||||
is ManageTokensListConfig.Account -> createManagedCryptoCurrencyList(
|
||||
params = params,
|
||||
userWallet = userWallet,
|
||||
isFirstBatchFetching = isFirstBatchFetching,
|
||||
loadUserTokensFromRemote = loadUserTokensFromRemote,
|
||||
query = query,
|
||||
updatedCoinsResponse = updatedCoinsResponse,
|
||||
)
|
||||
} else {
|
||||
managedCryptoCurrencyFactory.create(
|
||||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
is ManageTokensListConfig.Wallet -> createManagedCryptoCurrencyListLegacy(
|
||||
params = params,
|
||||
userWallet = userWallet,
|
||||
isFirstBatchFetching = isFirstBatchFetching,
|
||||
loadUserTokensFromRemote = loadUserTokensFromRemote,
|
||||
query = query,
|
||||
updatedCoinsResponse = updatedCoinsResponse,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -162,6 +162,112 @@ internal class DefaultManageTokensRepository(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun createManagedCryptoCurrencyList(
|
||||
params: ManageTokensListConfig.Account,
|
||||
userWallet: UserWallet?,
|
||||
isFirstBatchFetching: Boolean,
|
||||
loadUserTokensFromRemote: Boolean,
|
||||
query: String?,
|
||||
updatedCoinsResponse: CoinsResponse,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
val response = params.userWalletId?.let { userWalletId ->
|
||||
if (loadUserTokensFromRemote && userWallet != null) {
|
||||
runCatching { walletAccountsFetcher.fetch(userWalletId = userWallet.walletId) }.getOrNull()
|
||||
} else {
|
||||
walletAccountsFetcher.getSaved(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
val accountId = when {
|
||||
params.accountId == null -> null
|
||||
loadUserTokensFromRemote -> {
|
||||
AccountId.forCryptoPortfolio(
|
||||
userWalletId = requireNotNull(params.accountId).userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
else -> requireNotNull(params.accountId)
|
||||
}
|
||||
|
||||
val accountDTO = if (response != null && accountId != null) {
|
||||
response.accounts.firstOrNull { it.id == accountId.value }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val tokensResponse = response?.let {
|
||||
UserTokensResponse(
|
||||
group = response.wallet.group,
|
||||
sort = response.wallet.sort,
|
||||
tokens = accountDTO?.tokens.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
val isCreateWithCustom = isFirstBatchFetching &&
|
||||
tokensResponse != null &&
|
||||
userWallet != null &&
|
||||
query == null
|
||||
|
||||
val items = if (isCreateWithCustom) {
|
||||
managedCryptoCurrencyFactory.createWithCustomTokens(
|
||||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull(),
|
||||
)
|
||||
} else {
|
||||
managedCryptoCurrencyFactory.create(
|
||||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
private suspend fun createManagedCryptoCurrencyListLegacy(
|
||||
params: ManageTokensListConfig.Wallet,
|
||||
userWallet: UserWallet?,
|
||||
isFirstBatchFetching: Boolean,
|
||||
loadUserTokensFromRemote: Boolean,
|
||||
query: String?,
|
||||
updatedCoinsResponse: CoinsResponse,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
val tokensResponse = params.userWalletId?.let { userWalletId ->
|
||||
if (loadUserTokensFromRemote && userWallet != null) {
|
||||
safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) {
|
||||
// save tokens response only if loadUserTokensFromRemote is true and it means onboarding call
|
||||
createAndSaveDefaultUserTokensResponse(userWallet = userWallet)
|
||||
}
|
||||
} else {
|
||||
getSavedUserTokensResponseSync(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
val isCreateWithCustom = isFirstBatchFetching &&
|
||||
tokensResponse != null &&
|
||||
userWallet != null &&
|
||||
query == null
|
||||
|
||||
return if (isCreateWithCustom) {
|
||||
managedCryptoCurrencyFactory.createWithCustomTokens(
|
||||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = null,
|
||||
)
|
||||
} else {
|
||||
managedCryptoCurrencyFactory.create(
|
||||
coinsResponse = updatedCoinsResponse,
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createAndSaveDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse {
|
||||
val userTokensResponse = createDefaultUserTokensResponse(userWallet)
|
||||
userTokenSaver.store(userWallet.walletId, userTokensResponse, useEnricher = false)
|
||||
|
|
@ -170,9 +276,59 @@ internal class DefaultManageTokensRepository(
|
|||
|
||||
private suspend fun fetchTestnetCurrencies(
|
||||
userWallet: UserWallet,
|
||||
request: Request<ManageTokensListConfig>,
|
||||
params: ManageTokensListConfig.Account,
|
||||
): BatchFetchResult.Success<List<ManagedCryptoCurrency>> {
|
||||
val searchText = request.params.searchText
|
||||
val searchText = params.searchText
|
||||
val testnetTokensConfig = testnetTokensStorage.getConfig()
|
||||
|
||||
val response = params.userWalletId?.let { userWalletId ->
|
||||
walletAccountsFetcher.getSaved(userWalletId)
|
||||
}
|
||||
|
||||
val accountId = params.accountId
|
||||
|
||||
val accountDTO = if (response != null && accountId != null) {
|
||||
response.accounts.firstOrNull { it.id == accountId.value }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val tokensResponse = response?.let {
|
||||
UserTokensResponse(
|
||||
group = response.wallet.group,
|
||||
sort = response.wallet.sort,
|
||||
tokens = accountDTO?.tokens.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
val items = managedCryptoCurrencyFactory.createTestnetWithCustomTokens(
|
||||
testnetTokensConfig = if (!searchText.isNullOrBlank()) {
|
||||
testnetTokensConfig.copy(
|
||||
tokens = testnetTokensConfig.tokens.filter { token ->
|
||||
token.symbol.contains(other = searchText, ignoreCase = true) ||
|
||||
token.name.contains(other = searchText, ignoreCase = true)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
testnetTokensConfig
|
||||
},
|
||||
tokensResponse = tokensResponse,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull(),
|
||||
)
|
||||
|
||||
return BatchFetchResult.Success(
|
||||
data = items,
|
||||
empty = items.isEmpty(),
|
||||
last = true,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchTestnetCurrenciesLegacy(
|
||||
userWallet: UserWallet,
|
||||
params: ManageTokensListConfig.Wallet,
|
||||
): BatchFetchResult.Success<List<ManagedCryptoCurrency>> {
|
||||
val searchText = params.searchText
|
||||
val testnetTokensConfig = testnetTokensStorage.getConfig()
|
||||
|
||||
val items = managedCryptoCurrencyFactory.createTestnetWithCustomTokens(
|
||||
|
|
@ -188,6 +344,7 @@ internal class DefaultManageTokensRepository(
|
|||
},
|
||||
tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId),
|
||||
userWallet = userWallet,
|
||||
accountIndex = null,
|
||||
)
|
||||
|
||||
return BatchFetchResult.Success(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.managetokens.di
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
|
|
@ -11,6 +12,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
|
||||
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.managetokens.repository.CustomTokensRepository
|
||||
import com.tangem.domain.managetokens.repository.ManageTokensRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
|
@ -38,6 +40,8 @@ internal object ManageTokensDataModule {
|
|||
excludedBlockchains: ExcludedBlockchains,
|
||||
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
networkFactory: NetworkFactory,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
walletAccountsFetcher: WalletAccountsFetcher,
|
||||
): ManageTokensRepository {
|
||||
return DefaultManageTokensRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
|
|
@ -50,6 +54,8 @@ internal object ManageTokensDataModule {
|
|||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
networkFactory = networkFactory,
|
||||
dispatchers = dispatchers,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
walletAccountsFetcher = walletAccountsFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import com.tangem.data.common.network.NetworkFactory
|
|||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.card.common.extensions.canHandleToken
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -26,15 +28,17 @@ import timber.log.Timber
|
|||
internal class ManagedCryptoCurrencyFactory(
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
coinsResponse: CoinsResponse,
|
||||
tokensResponse: UserTokensResponse?,
|
||||
userWallet: UserWallet?,
|
||||
accountIndex: DerivationIndex?,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
return coinsResponse.coins.mapNotNull { coin ->
|
||||
createToken(coin, tokensResponse, coinsResponse.imageHost, userWallet)
|
||||
createToken(coin, tokensResponse, coinsResponse.imageHost, userWallet, accountIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,9 +46,10 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
coinsResponse: CoinsResponse,
|
||||
tokensResponse: UserTokensResponse,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex?,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
val customTokens = createCustomTokens(tokensResponse, userWallet)
|
||||
val tokens = create(coinsResponse, tokensResponse, userWallet)
|
||||
val customTokens = createCustomTokens(tokensResponse, userWallet, accountIndex)
|
||||
val tokens = create(coinsResponse, tokensResponse, userWallet, accountIndex)
|
||||
|
||||
return customTokens + tokens
|
||||
}
|
||||
|
|
@ -53,9 +58,10 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
testnetTokensConfig: TestnetTokensConfig,
|
||||
tokensResponse: UserTokensResponse?,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex?,
|
||||
): List<ManagedCryptoCurrency> {
|
||||
val customTokens = tokensResponse
|
||||
?.let { createCustomTokens(it, userWallet) }
|
||||
?.let { createCustomTokens(it, userWallet, accountIndex) }
|
||||
?: emptyList()
|
||||
val testnetTokens = testnetTokensConfig.tokens.map { testnetToken ->
|
||||
ManagedCryptoCurrency.Token(
|
||||
|
|
@ -69,9 +75,10 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
contractAddress = network.address,
|
||||
decimals = network.decimalCount,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
} ?: emptyList(),
|
||||
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, userWallet),
|
||||
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, userWallet, accountIndex),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -81,28 +88,47 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
private fun createCustomTokens(
|
||||
tokensResponse: UserTokensResponse,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex?,
|
||||
): List<ManagedCryptoCurrency> = tokensResponse.tokens
|
||||
.mapNotNull { token ->
|
||||
maybeCreateCustomToken(token, userWallet)
|
||||
maybeCreateCustomToken(token, userWallet, accountIndex)
|
||||
}
|
||||
|
||||
private fun maybeCreateCustomToken(
|
||||
token: UserTokensResponse.Token,
|
||||
userWallet: UserWallet,
|
||||
accountIndex: DerivationIndex?,
|
||||
): ManagedCryptoCurrency? {
|
||||
val blockchain = Blockchain.fromNetworkId(token.networkId)
|
||||
?.takeUnless { it in excludedBlockchains }
|
||||
?: return null
|
||||
|
||||
if (!checkIsCustomToken(token, blockchain, userWallet.derivationStyleProvider)) {
|
||||
return null
|
||||
val network = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val network = networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = token.derivationPath,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
) ?: return null
|
||||
|
||||
if (!checkIsCustomToken(token, network.derivationPath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
network
|
||||
} else {
|
||||
if (!checkIsCustomToken(token, blockchain, userWallet.derivationStyleProvider)) {
|
||||
return null
|
||||
}
|
||||
|
||||
networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = token.derivationPath,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
) ?: return null
|
||||
}
|
||||
|
||||
val network = networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = token.derivationPath,
|
||||
userWallet = userWallet,
|
||||
) ?: return null
|
||||
val contractAddress = token.contractAddress
|
||||
|
||||
return if (contractAddress.isNullOrBlank()) {
|
||||
|
|
@ -135,6 +161,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
tokensResponse: UserTokensResponse?,
|
||||
imageHost: String?,
|
||||
userWallet: UserWallet?,
|
||||
accountIndex: DerivationIndex?,
|
||||
): ManagedCryptoCurrency? {
|
||||
if (coinResponse.networks.isEmpty() || !coinResponse.active) return null
|
||||
|
||||
|
|
@ -146,6 +173,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
contractAddress = network.contractAddress,
|
||||
decimals = network.decimalCount?.toInt(),
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
}
|
||||
.ifEmpty { return null }
|
||||
|
|
@ -156,7 +184,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
symbol = coinResponse.symbol,
|
||||
iconUrl = getIconUrl(coinResponse.id, imageHost),
|
||||
availableNetworks = availableNetworks,
|
||||
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, userWallet),
|
||||
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, userWallet, accountIndex),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -166,6 +194,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
decimals: Int?,
|
||||
userWallet: UserWallet?,
|
||||
extraDerivationPath: String? = null,
|
||||
accountIndex: DerivationIndex?,
|
||||
): SourceNetwork? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
?.takeUnless { it in excludedBlockchains }
|
||||
|
|
@ -176,6 +205,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
extraDerivationPath = extraDerivationPath,
|
||||
derivationStyleProvider = userWallet?.derivationStyleProvider,
|
||||
canHandleTokens = userWallet?.canHandleToken(blockchain, excludedBlockchains) == true,
|
||||
accountIndex = accountIndex,
|
||||
) ?: return null
|
||||
|
||||
return when {
|
||||
|
|
@ -205,6 +235,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
currencyId: String,
|
||||
tokensResponse: UserTokensResponse?,
|
||||
userWallet: UserWallet?,
|
||||
accountIndex: DerivationIndex?,
|
||||
): Set<Network> {
|
||||
if (tokensResponse == null) return emptySet()
|
||||
|
||||
|
|
@ -219,6 +250,7 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
extraDerivationPath = token.derivationPath,
|
||||
derivationStyleProvider = userWallet?.derivationStyleProvider,
|
||||
canHandleTokens = userWallet?.canHandleToken(blockchain, excludedBlockchains) != false,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
@ -237,6 +269,10 @@ internal class ManagedCryptoCurrencyFactory(
|
|||
): Boolean = token.id.isNullOrBlank() ||
|
||||
checkIsCustomDerivationPath(token.derivationPath, blockchain, derivationStyleProvider)
|
||||
|
||||
private fun checkIsCustomToken(token: UserTokensResponse.Token, derivationPath: Network.DerivationPath): Boolean {
|
||||
return token.id.isNullOrBlank() || derivationPath is Network.DerivationPath.Custom
|
||||
}
|
||||
|
||||
private fun checkIsCustomDerivationPath(
|
||||
derivationPath: String?,
|
||||
blockchain: Blockchain,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.tangem.datasource.local.datastore.RuntimeStateStore
|
|||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.pagination.*
|
||||
|
|
@ -250,6 +251,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
userWalletId: UserWalletId,
|
||||
token: TokenMarketParams,
|
||||
network: TokenMarketInfo.Network,
|
||||
accountIndex: DerivationIndex?,
|
||||
): CryptoCurrency? {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("UserWalletId [$userWalletId] not found")
|
||||
val blockchain = Blockchain.fromNetworkId(network.networkId) ?: error("Unknown network [${network.networkId}]")
|
||||
|
|
@ -259,12 +261,14 @@ internal class DefaultMarketsTokenRepository(
|
|||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
} else {
|
||||
val currencyNetwork = networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
) ?: return null
|
||||
|
||||
cryptoCurrencyFactory.createToken(
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ dependencies {
|
|||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.express.models)
|
||||
|
||||
// region DI
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import com.tangem.data.onramp.converters.error.OnrampErrorConverter
|
|||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.TangemExpressValues
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeProvider
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeProviderType
|
||||
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
||||
|
|
@ -38,6 +37,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
|
|||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -560,7 +560,7 @@ internal class DefaultOnrampRepository(
|
|||
}
|
||||
|
||||
private fun CryptoCurrency.getContractAddress(): String = when (this) {
|
||||
is CryptoCurrency.Coin -> TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
is CryptoCurrency.Coin -> ExpressAsset.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
is CryptoCurrency.Token -> this.contractAddress
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ internal class DefaultOnrampTransactionRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getAllTransactions(): Flow<List<OnrampTransaction>> {
|
||||
return appPreferencesStore
|
||||
.getObjectSet<OnrampTransactionDTO>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
|
||||
.map { transactions -> transactions.map(transactionConverter::convert) }
|
||||
}
|
||||
|
||||
override suspend fun getTransactionById(txId: String): OnrampTransaction? = withContext(dispatchers.io) {
|
||||
val stored = appPreferencesStore.getObjectSetSync<OnrampTransactionDTO>(
|
||||
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ internal class HotCryptoCurrencyConverter(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO account
|
||||
private fun createNetwork(networkId: String): Network? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
|
||||
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ public val Blockchain.mercuryoNetwork: String?
|
|||
Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> null
|
||||
Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> null
|
||||
Blockchain.Quai, Blockchain.QuaiTestnet -> null
|
||||
// Blockchain.Linea, Blockchain.LineaTestnet -> null
|
||||
// Blockchain.ArbitrumNova -> null
|
||||
Blockchain.Linea, Blockchain.LineaTestnet -> null
|
||||
Blockchain.ArbitrumNova -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ dependencies {
|
|||
implementation(projects.domain.networks)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.account)
|
||||
|
||||
/** Tangem SDK */
|
||||
implementation(tangemDeps.blockchain) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
|
|||
import com.tangem.datasource.local.preferences.utils.getObjectList
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -27,6 +30,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
internal class DefaultSwapTransactionRepository(
|
||||
|
|
@ -34,6 +38,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val multiAccountListSupplier: MultiAccountListSupplier,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
) : SwapTransactionRepository {
|
||||
|
||||
private val listConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
|
|
@ -54,13 +60,15 @@ internal class DefaultSwapTransactionRepository(
|
|||
userWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
transaction: SwapTransactionModel,
|
||||
) {
|
||||
transaction.status?.let {
|
||||
storeTransactionState(
|
||||
txId = transaction.txId,
|
||||
status = it,
|
||||
accountWithCurrency = null,
|
||||
accountWithCurrency = fromAccount?.accountId to fromCryptoCurrency,
|
||||
)
|
||||
}
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
|
|
@ -87,12 +95,16 @@ internal class DefaultSwapTransactionRepository(
|
|||
userWalletId = userWalletId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = fromAccount,
|
||||
toAccount = toAccount,
|
||||
transactions = tokenTransactions,
|
||||
) ?: listOf(
|
||||
listConverter.default(
|
||||
userWalletId = userWalletId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = fromAccount,
|
||||
toAccount = toAccount,
|
||||
tokenTransactions = listOf(converter.convert(transaction)),
|
||||
),
|
||||
),
|
||||
|
|
@ -110,7 +122,12 @@ internal class DefaultSwapTransactionRepository(
|
|||
flow2 = appPreferencesStore.getObjectMap<SwapStatusDTO>(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
|
||||
),
|
||||
) { savedTransactions, txStatuses ->
|
||||
flow3 = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
multiAccountListSupplier()
|
||||
} else {
|
||||
flowOf(emptyList())
|
||||
},
|
||||
) { savedTransactions, txStatuses, multiAccountList ->
|
||||
val currencyTxs = savedTransactions
|
||||
?.filter {
|
||||
it.userWalletId == userWallet.walletId.stringValue &&
|
||||
|
|
@ -123,6 +140,7 @@ internal class DefaultSwapTransactionRepository(
|
|||
currencyTxs?.mapNotNull {
|
||||
listConverter.convertBack(
|
||||
value = it,
|
||||
multiAccountList = multiAccountList,
|
||||
userWallet = userWallet,
|
||||
txStatuses = txStatuses,
|
||||
)
|
||||
|
|
@ -221,10 +239,13 @@ internal class DefaultSwapTransactionRepository(
|
|||
fromCryptoCurrencyId == fromCurrencyId.value
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun List<SwapTransactionListDTO>.updateList(
|
||||
userWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
transactions: List<SwapTransactionDTO>,
|
||||
): List<SwapTransactionListDTO> {
|
||||
return addOrReplace(
|
||||
|
|
@ -232,6 +253,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
userWalletId = userWalletId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = fromAccount,
|
||||
toAccount = toAccount,
|
||||
tokenTransactions = transactions,
|
||||
),
|
||||
predicate = {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import com.tangem.data.swap.models.SwapStatusDTO
|
|||
import com.tangem.data.swap.models.SwapTransactionDTO
|
||||
import com.tangem.data.swap.models.SwapTransactionListDTO
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -32,11 +35,11 @@ internal class SavedSwapTransactionListConverter(
|
|||
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||
currency = value.fromCryptoCurrency,
|
||||
accountId = null,
|
||||
accountId = value.fromAccount?.accountId,
|
||||
),
|
||||
toTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||
currency = value.toCryptoCurrency,
|
||||
accountId = null,
|
||||
accountId = value.toAccount?.accountId,
|
||||
),
|
||||
transactions = savedSwapTransactionConverter.convertList(value.transactions),
|
||||
)
|
||||
|
|
@ -45,9 +48,12 @@ internal class SavedSwapTransactionListConverter(
|
|||
value: SwapTransactionListDTO,
|
||||
userWallet: UserWallet,
|
||||
txStatuses: Map<String, SwapStatusDTO>,
|
||||
multiAccountList: List<AccountList>,
|
||||
): SwapTransactionListModel? {
|
||||
val fromToken = value.fromTokensResponse
|
||||
val toToken = value.toTokensResponse
|
||||
val fromDerivationIndex = fromToken?.getDerivationIndex()
|
||||
val toDerivationIndex = toToken?.getDerivationIndex()
|
||||
return if (fromToken == null || toToken == null) {
|
||||
null
|
||||
} else {
|
||||
|
|
@ -73,14 +79,25 @@ internal class SavedSwapTransactionListConverter(
|
|||
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = getAccountByDerivationIndex(
|
||||
multiAccountList = multiAccountList,
|
||||
derivationIndex = fromDerivationIndex,
|
||||
),
|
||||
toAccount = getAccountByDerivationIndex(
|
||||
multiAccountList = multiAccountList,
|
||||
derivationIndex = toDerivationIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun default(
|
||||
userWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account.CryptoPortfolio?,
|
||||
toAccount: Account.CryptoPortfolio?,
|
||||
tokenTransactions: List<SwapTransactionDTO>,
|
||||
) = SwapTransactionListDTO(
|
||||
userWalletId = userWalletId.stringValue,
|
||||
|
|
@ -88,9 +105,12 @@ internal class SavedSwapTransactionListConverter(
|
|||
toCryptoCurrencyId = toCryptoCurrency.id.value,
|
||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||
currency = fromCryptoCurrency,
|
||||
accountId = null,
|
||||
accountId = fromAccount?.accountId,
|
||||
),
|
||||
toTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||
currency = toCryptoCurrency,
|
||||
accountId = toAccount?.accountId,
|
||||
),
|
||||
toTokensResponse = userTokensResponseFactory.createResponseToken(currency = toCryptoCurrency, accountId = null),
|
||||
transactions = tokenTransactions,
|
||||
)
|
||||
|
||||
|
|
@ -108,7 +128,24 @@ internal class SavedSwapTransactionListConverter(
|
|||
blockchain = blockchain,
|
||||
extraDerivationPath = token.derivationPath,
|
||||
userWallet = userWallet,
|
||||
accountIndex = token.accountId?.toIntOrNull()?.let { DerivationIndex(it).getOrNull() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAccountByDerivationIndex(
|
||||
multiAccountList: List<AccountList>,
|
||||
derivationIndex: DerivationIndex?,
|
||||
): Account.CryptoPortfolio? {
|
||||
if (derivationIndex == null) return null
|
||||
|
||||
return multiAccountList.asSequence().firstNotNullOfOrNull { accountList ->
|
||||
accountList.accounts.asSequence().filterIsInstance<Account.CryptoPortfolio>()
|
||||
.firstOrNull { it.derivationIndex == derivationIndex }
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.Token.getDerivationIndex(): DerivationIndex? {
|
||||
return accountId?.toIntOrNull()?.let { DerivationIndex(it).getOrNull() }
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
|||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.express.ExpressRepository
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
|
|
@ -68,12 +70,16 @@ internal object SwapDataModule {
|
|||
appPreferencesStore: AppPreferencesStore,
|
||||
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
networkFactory: NetworkFactory,
|
||||
multiAccountListSupplier: MultiAccountListSupplier,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): SwapTransactionRepository {
|
||||
return DefaultSwapTransactionRepository(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
networkFactory = networkFactory,
|
||||
multiAccountListSupplier = multiAccountListSupplier,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ dependencies {
|
|||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.express)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.staking)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
package com.tangem.data.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.datasource.api.tangemTech.models.account.flattenTokens
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
|
|
@ -15,6 +19,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
*
|
||||
* @property userWalletsStore [UserWallet]'s store
|
||||
* @property walletAccountsFetcher instance of [WalletAccountsFetcher] to fetch accounts for a multi wallet
|
||||
* @property expressServiceFetcher fetcher of express service
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -22,14 +27,26 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
internal class AccountListCryptoCurrenciesFetcher(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher,
|
||||
private val expressServiceFetcher: ExpressServiceFetcher,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiWalletCryptoCurrenciesFetcher {
|
||||
|
||||
override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
override suspend fun invoke(params: Params): Either<Throwable, Unit> {
|
||||
return Either.catchOn(dispatchers.default) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
|
||||
if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet")
|
||||
if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet")
|
||||
|
||||
walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
|
||||
val response = walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
|
||||
|
||||
expressServiceFetcher.fetch(
|
||||
userWallet = userWallet,
|
||||
assetIds = response.flattenTokens().mapTo(hashSetOf()) {
|
||||
ExpressAsset.ID(networkId = it.networkId, contractAddress = it.contractAddress)
|
||||
},
|
||||
)
|
||||
|
||||
Unit.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,15 +7,14 @@ import com.tangem.data.common.currency.UserTokensResponseFactory
|
|||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.data.tokens.utils.CustomTokensMerger
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
|
|
@ -32,7 +31,7 @@ import timber.log.Timber
|
|||
* @property userTokensResponseStore store of [UserTokensResponse]
|
||||
* @property userTokensSaver user tokens saver
|
||||
* @property cardCryptoCurrencyFactory factory for creating crypto currencies for specified card
|
||||
* @property expressServiceLoader express service loader
|
||||
* @property expressServiceFetcher express service loader
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -46,7 +45,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
|
|||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
private val expressServiceLoader: ExpressServiceLoader,
|
||||
private val expressServiceFetcher: ExpressServiceFetcher,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiWalletCryptoCurrenciesFetcher {
|
||||
|
||||
|
|
@ -109,14 +108,14 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
|
|||
}
|
||||
|
||||
private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) {
|
||||
val tokens = userTokens.tokens.map { token ->
|
||||
LeastTokenInfo(
|
||||
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
|
||||
network = token.networkId,
|
||||
val tokens = userTokens.tokens.mapTo(hashSetOf()) { token ->
|
||||
ExpressAsset.ID(
|
||||
networkId = token.networkId,
|
||||
contractAddress = token.contractAddress,
|
||||
)
|
||||
}
|
||||
|
||||
expressServiceLoader.update(userWallet = userWallet, userTokens = tokens)
|
||||
expressServiceFetcher.fetch(userWallet = userWallet, assetIds = tokens)
|
||||
}
|
||||
|
||||
private fun createDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse {
|
||||
|
|
@ -124,6 +123,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
|
|||
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
accountId = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,11 +7,11 @@ import com.tangem.data.tokens.AccountListCryptoCurrenciesFetcher
|
|||
import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.data.tokens.utils.CustomTokensMerger
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
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.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -33,7 +33,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule {
|
|||
userTokensResponseStore: UserTokensResponseStore,
|
||||
userTokensSaver: UserTokensSaver,
|
||||
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
expressServiceLoader: ExpressServiceLoader,
|
||||
expressServiceFetcher: ExpressServiceFetcher,
|
||||
walletAccountsFetcher: WalletAccountsFetcher,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): MultiWalletCryptoCurrenciesFetcher {
|
||||
|
|
@ -41,6 +41,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule {
|
|||
AccountListCryptoCurrenciesFetcher(
|
||||
userWalletsStore = userWalletsStore,
|
||||
walletAccountsFetcher = walletAccountsFetcher,
|
||||
expressServiceFetcher = expressServiceFetcher,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -56,7 +57,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule {
|
|||
userTokensResponseStore = userTokensResponseStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
expressServiceFetcher = expressServiceFetcher,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,14 @@ import com.tangem.data.common.cache.CacheRegistry
|
|||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
|
||||
import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository
|
||||
import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository
|
||||
import com.tangem.data.tokens.repository.DefaultYieldSupplyWarningsViewedRepository
|
||||
import com.tangem.data.tokens.repository.*
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.TokenReceiveWarningActionStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -40,7 +34,7 @@ internal object TokensDataModule {
|
|||
walletManagersFacade: WalletManagersFacade,
|
||||
cacheRegistry: CacheRegistry,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
expressServiceLoader: ExpressServiceLoader,
|
||||
expressServiceFetcher: ExpressServiceFetcher,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
tokensSaver: UserTokensSaver,
|
||||
|
|
@ -52,7 +46,7 @@ internal object TokensDataModule {
|
|||
walletManagersFacade = walletManagersFacade,
|
||||
cacheRegistry = cacheRegistry,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
expressServiceFetcher = expressServiceFetcher,
|
||||
dispatchers = dispatchers,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
|
|
|
|||
|
|
@ -10,17 +10,16 @@ import com.tangem.data.common.currency.*
|
|||
import com.tangem.data.tokens.utils.CustomTokensMerger
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.CardTypesResolver
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -40,7 +39,7 @@ internal class DefaultCurrenciesRepository(
|
|||
private val userWalletsStore: UserWalletsStore,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val expressServiceLoader: ExpressServiceLoader,
|
||||
private val expressServiceFetcher: ExpressServiceFetcher,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
|
|
@ -74,26 +73,6 @@ internal class DefaultCurrenciesRepository(
|
|||
userTokensSaver.storeAndPush(userWalletId, response)
|
||||
}
|
||||
|
||||
override suspend fun saveCurrenciesLocal(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
withContext(dispatchers.io) {
|
||||
val savedResponse = requireNotNull(
|
||||
value = getSavedUserTokensResponseSync(key = userWalletId),
|
||||
lazyMessage = { "Saved tokens empty. Can not perform add currencies action." },
|
||||
)
|
||||
|
||||
val updatedResponse = savedResponse.copy(
|
||||
tokens = currencies.map(userTokensResponseFactory::createResponseToken),
|
||||
)
|
||||
|
||||
userTokensSaver.store(userWalletId = userWalletId, response = updatedResponse)
|
||||
|
||||
fetchExpressAssetsByNetworkIds(
|
||||
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
|
||||
userTokens = updatedResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun addCurrenciesCache(
|
||||
userWalletId: UserWalletId,
|
||||
currencies: List<CryptoCurrency>,
|
||||
|
|
@ -322,50 +301,6 @@ internal class DefaultCurrenciesRepository(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) =
|
||||
withContext(dispatchers.io) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
val storedTokens = requireNotNull(
|
||||
value = getSavedUserTokensResponseSync(key = userWallet.walletId),
|
||||
lazyMessage = {
|
||||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
},
|
||||
)
|
||||
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
storedTokens,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
): CryptoCurrency = withContext(dispatchers.io) {
|
||||
getMultiCurrencyWalletCurrency(userWalletId, id.value)
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency =
|
||||
withContext(dispatchers.io) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
val response = requireNotNull(
|
||||
value = getSavedUserTokensResponseSync(key = userWalletId),
|
||||
lazyMessage = {
|
||||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
},
|
||||
)
|
||||
|
||||
responseCryptoCurrenciesFactory.createCurrency(
|
||||
currencyId = id,
|
||||
response = response,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getNetworkCoin(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
|
|
@ -667,16 +602,17 @@ internal class DefaultCurrenciesRepository(
|
|||
return demoConfig.isDemoCardId(userWallet.cardId) && response == null
|
||||
}
|
||||
|
||||
// TODO [REDACTED_JIRA]
|
||||
private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) {
|
||||
val tokens = userTokens.tokens.map { token ->
|
||||
LeastTokenInfo(
|
||||
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
|
||||
network = token.networkId,
|
||||
val tokens = userTokens.tokens.mapTo(hashSetOf()) { token ->
|
||||
ExpressAsset.ID(
|
||||
networkId = token.networkId,
|
||||
contractAddress = token.contractAddress,
|
||||
)
|
||||
}
|
||||
|
||||
coroutineScope {
|
||||
launch { expressServiceLoader.update(userWallet, tokens) }
|
||||
launch { expressServiceFetcher.fetch(userWallet, tokens) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -685,11 +621,11 @@ internal class DefaultCurrenciesRepository(
|
|||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
refresh: Boolean = false,
|
||||
) {
|
||||
val tokens = cryptoCurrencies.map { currency ->
|
||||
val tokens = cryptoCurrencies.mapTo(hashSetOf()) { currency ->
|
||||
val tokenCurrency = currency as? CryptoCurrency.Token
|
||||
LeastTokenInfo(
|
||||
contractAddress = tokenCurrency?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
|
||||
network = currency.network.backendId,
|
||||
ExpressAsset.ID(
|
||||
networkId = currency.network.backendId,
|
||||
contractAddress = tokenCurrency?.contractAddress,
|
||||
)
|
||||
}
|
||||
cacheRegistry.invokeOnExpire(
|
||||
|
|
@ -697,7 +633,7 @@ internal class DefaultCurrenciesRepository(
|
|||
skipCache = refresh,
|
||||
block = {
|
||||
coroutineScope {
|
||||
launch { expressServiceLoader.update(userWallet, tokens) }
|
||||
launch { expressServiceFetcher.fetch(userWallet, tokens) }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -731,6 +667,7 @@ internal class DefaultCurrenciesRepository(
|
|||
),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
accountId = null,
|
||||
)
|
||||
|
||||
private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.data.tokens
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.common.test.utils.assertEither
|
||||
import com.tangem.common.test.utils.assertEitherRight
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
|
|
@ -21,11 +24,13 @@ internal class AccountListCryptoCurrenciesFetcherTest {
|
|||
|
||||
private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true)
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher = mockk(relaxUnitFun = true)
|
||||
private val expressServiceFetcher: ExpressServiceFetcher = mockk()
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val fetcher = AccountListCryptoCurrenciesFetcher(
|
||||
userWalletsStore = userWalletsStore,
|
||||
walletAccountsFetcher = walletAccountsFetcher,
|
||||
expressServiceFetcher = expressServiceFetcher,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
|
|
@ -59,8 +64,11 @@ internal class AccountListCryptoCurrenciesFetcherTest {
|
|||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
val mockUserWallet = mockk<UserWallet> { every { isMultiCurrency } returns true }
|
||||
val response = mockk<GetWalletAccountsResponse>(relaxed = true)
|
||||
|
||||
every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet
|
||||
coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } returns response
|
||||
coEvery { expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet()) } returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
|
@ -71,6 +79,7 @@ internal class AccountListCryptoCurrenciesFetcherTest {
|
|||
coVerify(ordering = Ordering.SEQUENCE) {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
|
||||
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,14 +11,13 @@ import com.tangem.data.common.currency.UserTokensSaver
|
|||
import com.tangem.data.tokens.utils.CustomTokensMerger
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.express.models.ExpressAsset
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
|
|
@ -45,7 +44,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
|
||||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
|
||||
private val expressServiceLoader: ExpressServiceLoader = mockk(relaxUnitFun = true)
|
||||
private val expressServiceFetcher: ExpressServiceFetcher = mockk(relaxUnitFun = true)
|
||||
|
||||
private val fetcher = DefaultMultiWalletCryptoCurrenciesFetcher(
|
||||
demoConfig = DemoConfig(),
|
||||
|
|
@ -55,7 +54,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
userTokensResponseStore = userTokensResponseStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
expressServiceFetcher = expressServiceFetcher,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
|
|
@ -67,7 +66,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
userTokensResponseStore,
|
||||
userTokensSaver,
|
||||
cardCryptoCurrencyFactory,
|
||||
expressServiceLoader,
|
||||
expressServiceFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +130,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
} returns userTokensResponse
|
||||
|
||||
coEvery {
|
||||
expressServiceFetcher.fetch(
|
||||
userWallet = mockUserWallet,
|
||||
assetIds = userTokensResponse.toAssetId(),
|
||||
)
|
||||
} returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
|
|
@ -144,7 +150,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens())
|
||||
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,6 +176,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
|
||||
} returns apiResponse.data
|
||||
|
||||
coEvery {
|
||||
expressServiceFetcher.fetch(
|
||||
userWallet = mockUserWallet,
|
||||
assetIds = defaultResponse.toAssetId(),
|
||||
)
|
||||
} returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
|
|
@ -183,7 +196,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
|
||||
userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data)
|
||||
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens())
|
||||
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
|
|
@ -212,6 +225,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
|
||||
} returns apiResponse.data
|
||||
|
||||
coEvery {
|
||||
expressServiceFetcher.fetch(
|
||||
userWallet = mockUserWallet,
|
||||
assetIds = apiResponse.data.toAssetId(),
|
||||
)
|
||||
} returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
|
|
@ -224,7 +244,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
|
||||
userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data)
|
||||
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens())
|
||||
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
|
|
@ -273,6 +293,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
} returns userTokensResponse
|
||||
|
||||
coEvery {
|
||||
expressServiceFetcher.fetch(
|
||||
userWallet = mockUserWallet,
|
||||
assetIds = userTokensResponse.toAssetId(),
|
||||
)
|
||||
} returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
|
|
@ -286,7 +313,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens())
|
||||
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
|
|
@ -317,6 +344,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
|
||||
} returns defaultResponse
|
||||
|
||||
coEvery {
|
||||
expressServiceFetcher.fetch(
|
||||
userWallet = mockUserWallet,
|
||||
assetIds = defaultResponse.toAssetId(),
|
||||
)
|
||||
} returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
|
|
@ -330,7 +364,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
|
||||
userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse)
|
||||
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens())
|
||||
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
|
|
@ -383,6 +417,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
} returns userTokensResponse
|
||||
|
||||
coEvery {
|
||||
expressServiceFetcher.fetch(
|
||||
userWallet = mockUserWallet,
|
||||
assetIds = userTokensResponse.toAssetId(),
|
||||
)
|
||||
} returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
|
|
@ -398,7 +439,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
userTokensSaver.push(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens())
|
||||
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -429,6 +470,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
|
||||
} returns defaultResponse
|
||||
|
||||
coEvery {
|
||||
expressServiceFetcher.fetch(
|
||||
userWallet = mockUserWallet,
|
||||
assetIds = defaultResponse.toAssetId(),
|
||||
)
|
||||
} returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
|
|
@ -443,7 +491,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse)
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
|
||||
userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse)
|
||||
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens())
|
||||
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
|
|
@ -471,11 +519,11 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
|||
),
|
||||
)
|
||||
|
||||
fun UserTokensResponse.toLeastTokens(): List<LeastTokenInfo> {
|
||||
return tokens.map { token ->
|
||||
LeastTokenInfo(
|
||||
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
|
||||
network = token.networkId,
|
||||
fun UserTokensResponse.toAssetId(): Set<ExpressAsset.ID> {
|
||||
return tokens.mapTo(hashSetOf()) { token ->
|
||||
ExpressAsset.ID(
|
||||
networkId = token.networkId,
|
||||
contractAddress = token.contractAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ dependencies {
|
|||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.networks)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.quotes)
|
||||
|
||||
/** Feature API - remove after removing [HotWalletFeatureToggles] */
|
||||
implementation(projects.features.hotWallet.api)
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
package com.tangem.data.pay
|
||||
|
||||
import arrow.core.Either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.pay.util.TangemPayErrorConverter
|
||||
import com.tangem.data.pay.util.TangemPayWalletsManager
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.ReceiveAddressModel.NameService
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.pay.DataForReceive
|
||||
import com.tangem.domain.pay.DataForReceiveFactory
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "TangemPay: TokenReceiveConfigFactory"
|
||||
|
||||
internal class DefaultDataForReceiveFactory @Inject constructor(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
private val tangemPayWalletsManager: TangemPayWalletsManager,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
) : DataForReceiveFactory {
|
||||
|
||||
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
CryptoCurrencyFactory(excludedBlockchains)
|
||||
}
|
||||
private val errorConverter by lazy(mode = LazyThreadSafetyMode.NONE) { TangemPayErrorConverter(moshi) }
|
||||
|
||||
override fun getDataForReceive(depositAddress: String, chainId: Int): Either<UniversalError, DataForReceive> {
|
||||
return try {
|
||||
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking()
|
||||
|
||||
/**
|
||||
* Create [CryptoCurrency.Coin] only for F&F.
|
||||
* Later will use [CryptoCurrency.Token] when contractAddresses will be provided by BFF.
|
||||
*/
|
||||
val currency = cryptoCurrencyFactory.createCoin(
|
||||
chainId = chainId,
|
||||
extraDerivationPath = null,
|
||||
userWallet = wallet,
|
||||
) ?: error("Cannot create crypto currency from chainId $chainId")
|
||||
|
||||
val result = DataForReceive(
|
||||
currency = currency,
|
||||
walletId = wallet.walletId,
|
||||
receiveAddress = listOf(ReceiveAddressModel(nameService = NameService.Default, value = depositAddress)),
|
||||
)
|
||||
|
||||
Either.Right(result)
|
||||
} catch (exception: Exception) {
|
||||
when (exception) {
|
||||
is CancellationException -> {
|
||||
throw exception
|
||||
}
|
||||
else -> {
|
||||
Timber.tag(TAG).e(exception)
|
||||
Either.Left(errorConverter.convert(exception))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.data.pay
|
||||
|
||||
import arrow.core.Either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.blockchains.ethereum.Chain
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.pay.util.TangemPayErrorConverter
|
||||
import com.tangem.data.pay.util.TangemPayWalletsManager
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.ReceiveAddressModel.NameService
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.TangemPayTopUpData
|
||||
import com.tangem.domain.pay.TangemPayTopUpDataFactory
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "TangemPay: DefaultDataForTopUpFactory"
|
||||
/**
|
||||
* Custom token parameters. Will be used only for F&F.
|
||||
*/
|
||||
private const val TOKEN_ID = "usd-coin"
|
||||
private const val TOKEN_NAME = "USDC"
|
||||
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
||||
private const val TOKEN_DECIMALS = 6
|
||||
|
||||
internal class DefaultTangemPayTopUpDataFactory @Inject constructor(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
private val tangemPayWalletsManager: TangemPayWalletsManager,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
) : TangemPayTopUpDataFactory {
|
||||
|
||||
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
CryptoCurrencyFactory(excludedBlockchains)
|
||||
}
|
||||
private val networkFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
NetworkFactory(excludedBlockchains)
|
||||
}
|
||||
private val errorConverter by lazy(mode = LazyThreadSafetyMode.NONE) { TangemPayErrorConverter(moshi) }
|
||||
|
||||
private fun getCurrency(userWallet: UserWallet, chainId: Int): CryptoCurrency {
|
||||
val chain = requireNotNull(Chain.entries.find { it.id == chainId }) { "Can not find chain with $chainId" }
|
||||
val blockchain = requireNotNull(chain.blockchain)
|
||||
val network = networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
return cryptoCurrencyFactory.createToken(
|
||||
network = requireNotNull(network),
|
||||
rawId = CryptoCurrency.RawID(TOKEN_ID),
|
||||
name = TOKEN_NAME,
|
||||
symbol = TOKEN_NAME,
|
||||
contractAddress = TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TOKEN_DECIMALS,
|
||||
)
|
||||
}
|
||||
|
||||
override fun create(
|
||||
depositAddress: String,
|
||||
chainId: Int,
|
||||
cryptoBalance: BigDecimal,
|
||||
fiatBalance: BigDecimal,
|
||||
): Either<UniversalError, TangemPayTopUpData> {
|
||||
return try {
|
||||
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking()
|
||||
val currency = getCurrency(wallet, chainId)
|
||||
val result = TangemPayTopUpData(
|
||||
currency = currency,
|
||||
walletId = wallet.walletId,
|
||||
cryptoBalance = cryptoBalance,
|
||||
fiatBalance = fiatBalance,
|
||||
depositAddress = depositAddress,
|
||||
receiveAddress = listOf(ReceiveAddressModel(nameService = NameService.Default, value = depositAddress)),
|
||||
)
|
||||
|
||||
Either.Right(result)
|
||||
} catch (exception: Exception) {
|
||||
when (exception) {
|
||||
is CancellationException -> {
|
||||
throw exception
|
||||
}
|
||||
else -> {
|
||||
Timber.tag(TAG).e(exception)
|
||||
Either.Left(errorConverter.convert(exception))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,8 @@ 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.domain.visa.model.TangemPayInitialCredentials
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import javax.inject.Inject
|
||||
|
|
@ -16,29 +15,14 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
|
|||
private val tangemSdkManager: TangemSdkManager,
|
||||
) : TangemPayAuthDataSource {
|
||||
|
||||
override suspend fun generateNewAuthTokens(address: String, cardId: String): Either<Throwable, VisaAuthTokens> =
|
||||
either {
|
||||
val challenge = visaAuthRemoteDataSource
|
||||
.getCustomerWalletAuthChallenge(address)
|
||||
.mapLeft { IllegalStateException("TangemPay challenge failed. Error code: ${it.errorCode}") }
|
||||
.bind()
|
||||
override suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials> {
|
||||
val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)
|
||||
|
||||
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()
|
||||
return when (initialCredentials) {
|
||||
is CompletionResult.Failure<*> -> Either.Left(initialCredentials.error)
|
||||
is CompletionResult.Success<TangemPayInitialCredentials> -> Either.Right(initialCredentials.data)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, VisaAuthTokens> = either {
|
||||
visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens(
|
||||
|
|
@ -47,9 +31,4 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
|
|||
.mapLeft { IllegalStateException("TangemPay token refresh 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,16 +1,19 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.DefaultDataForReceiveFactory
|
||||
import com.tangem.data.pay.DefaultTangemPayTopUpDataFactory
|
||||
import com.tangem.data.pay.repository.DefaultCardDetailsRepository
|
||||
import com.tangem.data.pay.repository.DefaultKycRepository
|
||||
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
|
||||
import com.tangem.data.pay.repository.DefaultOnboardingRepository
|
||||
import com.tangem.domain.pay.DataForReceiveFactory
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.pay.TangemPayTopUpDataFactory
|
||||
import com.tangem.domain.pay.repository.CardDetailsRepository
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -41,7 +44,13 @@ internal interface TangemPayDataModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindDataForReceiveFactory(factory: DefaultDataForReceiveFactory): DataForReceiveFactory
|
||||
fun bindDataForReceiveFactory(factory: DefaultTangemPayTopUpDataFactory): TangemPayTopUpDataFactory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindGetTangemPayCurrencyStatusUseCase(
|
||||
impl: DefaultGetTangemPayCurrencyStatusUseCase,
|
||||
): GetTangemPayCurrencyStatusUseCase
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
|
|
@ -52,6 +61,14 @@ internal interface TangemPayDataModule {
|
|||
return TangemPayMainScreenCustomerInfoUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideProduceTangemPayInitialDataUseCase(
|
||||
repository: OnboardingRepository,
|
||||
): ProduceTangemPayInitialDataUseCase {
|
||||
return ProduceTangemPayInitialDataUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayIssueOrderUseCase(repository: OnboardingRepository): TangemPayIssueOrderUseCase {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.data.pay.entity
|
||||
|
||||
internal data class EncryptedData(
|
||||
val encryptedBase64: String,
|
||||
val ivBase64: String,
|
||||
)
|
||||
|
|
@ -9,6 +9,9 @@ import com.tangem.datasource.api.common.config.ApiEnvironment
|
|||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.CardDetailsRequest
|
||||
import com.tangem.datasource.api.pay.models.request.SetPinRequest
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.pay.model.SetPinResult
|
||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||
import com.tangem.domain.pay.model.TangemPayCardDetails
|
||||
import com.tangem.domain.pay.repository.CardDetailsRepository
|
||||
|
|
@ -22,6 +25,7 @@ internal class DefaultCardDetailsRepository @Inject constructor(
|
|||
private val visaLibLoader: VisaLibLoader,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
private val rainCryptoUtil: RainCryptoUtil,
|
||||
private val storage: TangemPayStorage,
|
||||
) : CardDetailsRepository {
|
||||
|
||||
override suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance> {
|
||||
|
|
@ -39,7 +43,7 @@ internal class DefaultCardDetailsRepository @Inject constructor(
|
|||
override suspend fun revealCardDetails(): Either<UniversalError, TangemPayCardDetails> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
val (secretKeyHex, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.revealCardDetails(
|
||||
|
|
@ -51,14 +55,15 @@ internal class DefaultCardDetailsRepository @Inject constructor(
|
|||
val pan = rainCryptoUtil.decryptSecret(
|
||||
base64Secret = result.pan.secret,
|
||||
base64Iv = result.pan.iv,
|
||||
secretKeyHex = secretKeyHex,
|
||||
secretKeyBytes = secretKeyBytes,
|
||||
)
|
||||
|
||||
val cvv = rainCryptoUtil.decryptSecret(
|
||||
base64Secret = result.cvv.secret,
|
||||
base64Iv = result.cvv.iv,
|
||||
secretKeyHex = secretKeyHex,
|
||||
secretKeyBytes = secretKeyBytes,
|
||||
)
|
||||
secretKeyBytes.fill(0)
|
||||
|
||||
TangemPayCardDetails(
|
||||
pan = pan,
|
||||
|
|
@ -69,6 +74,44 @@ internal class DefaultCardDetailsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun setPin(pin: String): Either<UniversalError, SetPinResult> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes)
|
||||
secretKeyBytes.fill(0)
|
||||
|
||||
val status = requestHelper.request { authHeader ->
|
||||
tangemPayApi.setPin(
|
||||
authHeader = authHeader,
|
||||
body = SetPinRequest(
|
||||
sessionId = sessionId,
|
||||
pin = encryptedData.encryptedBase64,
|
||||
iv = encryptedData.ivBase64,
|
||||
),
|
||||
)
|
||||
}.result?.result ?: error("Cannot set pin code")
|
||||
when (status) {
|
||||
SetPinResult.SUCCESS.name -> SetPinResult.SUCCESS
|
||||
SetPinResult.PIN_TOO_WEAK.name -> SetPinResult.PIN_TOO_WEAK
|
||||
SetPinResult.DECRYPTION_ERROR.name -> SetPinResult.DECRYPTION_ERROR
|
||||
else -> SetPinResult.UNKNOWN_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isAddToWalletDone(): Either<UniversalError, Boolean> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
storage.getAddToWalletDone(requestHelper.getCustomerWalletAddress())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setAddToWalletAsDone(): Either<UniversalError, Unit> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
storage.storeAddToWalletDone(requestHelper.getCustomerWalletAddress(), isDone = true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPublicKeyBase64(): String {
|
||||
val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment
|
||||
return when (env) {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package com.tangem.data.pay.repository
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.pay.util.TangemPayWalletsManager
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
|
||||
import com.tangem.datasource.api.pay.models.request.OrderRequest
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
|
||||
|
|
@ -26,17 +29,44 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val tangemPayStorage: TangemPayStorage,
|
||||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val tangemPayWalletsManager: TangemPayWalletsManager,
|
||||
) : OnboardingRepository {
|
||||
|
||||
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.request {
|
||||
tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link))
|
||||
}.result
|
||||
val result = tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link))
|
||||
.getOrThrow()
|
||||
.result
|
||||
result?.status == VALID_STATUS
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isTangemPayInitialDataProduced(): Boolean {
|
||||
val walletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId
|
||||
val customerWalletAddress = tangemPayStorage.getCustomerWalletAddress(walletId) ?: return false
|
||||
tangemPayStorage.getAuthTokens(customerWalletAddress) ?: return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun produceInitialData() {
|
||||
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay()
|
||||
val initialCredentials = authDataSource.produceInitialCredentials(cardId = wallet.cardId)
|
||||
.fold(
|
||||
ifLeft = { error -> error("Can not produce initial data: ${error.message}") },
|
||||
ifRight = { it },
|
||||
)
|
||||
tangemPayStorage.storeCustomerWalletAddress(
|
||||
userWalletId = wallet.walletId,
|
||||
customerWalletAddress = initialCredentials.customerWalletAddress,
|
||||
)
|
||||
tangemPayStorage.storeAuthTokens(
|
||||
customerWalletAddress = initialCredentials.customerWalletAddress,
|
||||
tokens = initialCredentials.authTokens,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.request { authHeader ->
|
||||
|
|
@ -84,7 +114,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
override suspend fun createOrder(): Either<UniversalError, Unit> = withContext(dispatcherProvider.io) {
|
||||
requestHelper.runWithErrorLogs(TAG) {
|
||||
val walletAddress = requestHelper.getCustomerWalletAddress()
|
||||
val result = requestHelper.requestWithPersistedToken { authHeader ->
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress))
|
||||
}.result ?: error("Create order result is null")
|
||||
|
||||
|
|
@ -128,7 +158,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun getCustomerInfoWithPersistedToken(): CustomerInfo {
|
||||
val result = requestHelper.requestWithPersistedToken { authHeader ->
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.getCustomerMe(authHeader)
|
||||
}.result
|
||||
return getCustomerInfo(result)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.utils.TangemPayTxHistoryItemConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
|
|
@ -26,8 +28,11 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
|||
private val cacheRegistry: CacheRegistry,
|
||||
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
) : TangemPayTxHistoryRepository {
|
||||
|
||||
private val txHistoryItemConverter by lazy { TangemPayTxHistoryItemConverter(moshi) }
|
||||
|
||||
override fun getTxHistoryBatchFlow(
|
||||
batchSize: Int,
|
||||
context: TangemPayTxHistoryListBatchingContext,
|
||||
|
|
@ -84,7 +89,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
|||
val result = requestPerformer.request { authHeader ->
|
||||
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
|
||||
}.result
|
||||
val items = TangemPayTxHistoryItemConverter.convertList(result.transactions).filterNotNull()
|
||||
val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull()
|
||||
txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items)
|
||||
}.onLeft { error(it.toString()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ package com.tangem.data.pay.repository
|
|||
|
||||
import arrow.core.Either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.pay.util.TangemPayErrorConverter
|
||||
import com.tangem.data.pay.util.TangemPayWalletsManager
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
|
|
@ -14,10 +12,10 @@ import com.tangem.datasource.di.NetworkMoshi
|
|||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.visa.model.getAuthHeader
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
|
|
@ -30,11 +28,9 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
private val tangemPayStorage: TangemPayStorage,
|
||||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val tangemPayWalletsManager: TangemPayWalletsManager,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val networkFactory: NetworkFactory,
|
||||
) {
|
||||
|
||||
private var customerWalletAddress: String? = null
|
||||
private val customerWalletAddress = MutableStateFlow<String?>(null)
|
||||
|
||||
private val refreshTokensMutex = Mutex()
|
||||
private var refreshTokensJob: Deferred<VisaAuthTokens>? = null
|
||||
|
|
@ -67,21 +63,14 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun <T : Any> requestWithPersistedToken(requestBlock: suspend (header: String) -> ApiResponse<T>): T =
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = { getAccessTokensIfSaved() ?: error("Cannot get saved access tokens") },
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> performRequest(
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
getTokens: (suspend () -> VisaAuthTokens),
|
||||
refreshTokens: (suspend () -> VisaAuthTokens)? = null,
|
||||
): T = runCatching {
|
||||
requestBlock("Bearer ${getTokens().accessToken}").getOrThrow()
|
||||
val tokens = getTokens()
|
||||
val header = tokens.getAuthHeader()
|
||||
requestBlock(header).getOrThrow()
|
||||
}.getOrElse { error ->
|
||||
val unauthorizedCode = ApiResponseError.HttpException.Code.UNAUTHORIZED
|
||||
if (error is ApiResponseError.HttpException && refreshTokens != null && error.code == unauthorizedCode) {
|
||||
|
|
@ -116,52 +105,34 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
return result
|
||||
}
|
||||
|
||||
suspend fun getCustomerWalletAddress(): String = customerWalletAddress ?: fetchAuthInputData().address
|
||||
suspend fun getCustomerWalletAddress(): String {
|
||||
val existingAddress = customerWalletAddress.value
|
||||
if (existingAddress != null) {
|
||||
return existingAddress
|
||||
}
|
||||
val storedAddress = tangemPayStorage.getCustomerWalletAddress(
|
||||
userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId,
|
||||
) ?: error("Can not find customer address")
|
||||
|
||||
customerWalletAddress.value = storedAddress
|
||||
return storedAddress
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokens(): VisaAuthTokens {
|
||||
return getAccessTokensIfSaved() ?: fetchTokens()
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokensIfSaved(): VisaAuthTokens? {
|
||||
return tangemPayStorage.getAuthTokens(getCustomerWalletAddress())
|
||||
}
|
||||
|
||||
private suspend fun fetchAuthInputData(): AuthInputData {
|
||||
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay()
|
||||
|
||||
val network = networkFactory.create(
|
||||
blockchain = Blockchain.Polygon,
|
||||
extraDerivationPath = null,
|
||||
derivationStyleProvider = wallet.derivationStyleProvider,
|
||||
canHandleTokens = true,
|
||||
) ?: error("Cannot create network")
|
||||
|
||||
val address = walletManagersFacade.getDefaultAddress(wallet.walletId, network)
|
||||
?: error("Cannot get polygon address")
|
||||
|
||||
customerWalletAddress = address
|
||||
|
||||
return AuthInputData(address, wallet.cardId)
|
||||
}
|
||||
|
||||
private suspend fun fetchTokens(): VisaAuthTokens {
|
||||
val inputData = fetchAuthInputData()
|
||||
val tokens = authDataSource.generateNewAuthTokens(inputData.address, inputData.cardId)
|
||||
.getOrNull() ?: error("Cannot fetch tokens")
|
||||
tangemPayStorage.storeAuthTokens(inputData.address, tokens)
|
||||
val walletAddress = getCustomerWalletAddress()
|
||||
val tokens = tangemPayStorage.getAuthTokens(walletAddress) ?: error("Auth tokens are not stored")
|
||||
return tokens
|
||||
}
|
||||
|
||||
private suspend fun refreshAuthTokens(): VisaAuthTokens {
|
||||
val customerWalletAddress = getCustomerWalletAddress()
|
||||
val refreshToken = getAccessTokens().refreshToken.value
|
||||
val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrNull() ?: error("Cannot refresh tokens")
|
||||
val tokens = authDataSource.refreshAuthTokens(refreshToken)
|
||||
.fold(
|
||||
ifLeft = { error -> error("Cannot refresh tokens: ${error.message}") },
|
||||
ifRight = { it },
|
||||
)
|
||||
tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens)
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
|
||||
internal data class AuthInputData(
|
||||
val address: String,
|
||||
val cardId: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.data.pay.usecase
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultGetTangemPayCurrencyStatusUseCase @Inject constructor(
|
||||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
|
||||
) : GetTangemPayCurrencyStatusUseCase {
|
||||
|
||||
override suspend fun invoke(
|
||||
currency: CryptoCurrency,
|
||||
cryptoAmount: BigDecimal,
|
||||
fiatAmount: BigDecimal,
|
||||
depositAddress: String,
|
||||
): CryptoCurrencyStatus? {
|
||||
val rawCurrencyId = currency.id.rawCurrencyId ?: return null
|
||||
val quoteStatus = singleQuoteStatusSupplier.getSyncOrNull(
|
||||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
|
||||
)?.value
|
||||
val quoteStatusData: QuoteStatus.Data = when (quoteStatus) {
|
||||
is QuoteStatus.Data -> quoteStatus
|
||||
else -> {
|
||||
singleQuoteStatusFetcher.invoke(
|
||||
params = SingleQuoteStatusFetcher.Params(rawCurrencyId = rawCurrencyId, appCurrencyId = null),
|
||||
)
|
||||
singleQuoteStatusSupplier.getSyncOrNull(
|
||||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
|
||||
)?.value as? QuoteStatus.Data
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
return CryptoCurrencyStatus(
|
||||
currency = currency,
|
||||
value = CryptoCurrencyStatus.Loaded(
|
||||
amount = cryptoAmount,
|
||||
fiatRate = quoteStatusData.fiatRate,
|
||||
priceChange = quoteStatusData.priceChange,
|
||||
fiatAmount = fiatAmount,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
value = depositAddress,
|
||||
),
|
||||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
yieldBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import android.util.Base64
|
||||
import com.tangem.utils.extensions.hexToBytes
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.KeyFactory
|
||||
import java.security.SecureRandom
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.data.pay.entity.EncryptedData
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
|
|
@ -21,26 +20,38 @@ private const val RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"
|
|||
private const val AES_TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
private const val AES_ALGORITHM = "AES"
|
||||
private const val RSA_ALGORITHM = "RSA"
|
||||
private const val IV_LENGTH_BYTES = 16
|
||||
private const val PIN_LENGTH = 4
|
||||
private const val PIN_LENGTH_BYTES = 8
|
||||
private const val PIN_BLOCK_ISO_9564_FORMAT_PREFIX = '2'
|
||||
private const val PIN_BLOCK_FILL_CHAR = 'F'
|
||||
|
||||
internal class RainCryptoUtil @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
suspend fun generateSecretKeyAndSessionId(publicKeyBase64: String): Pair<String, String> =
|
||||
suspend fun generateSecretKeyAndSessionId(publicKeyBase64: String): Pair<ByteArray, String> =
|
||||
withContext(dispatchers.default) {
|
||||
val secretKeyHex = ByteArray(KEY_LENGTH_BYTES).also { SecureRandom().nextBytes(it) }.toHexString()
|
||||
val sessionId = generateSessionId(publicKeyBase64, secretKeyHex)
|
||||
secretKeyHex to sessionId
|
||||
val secretKeyBytes = ByteArray(KEY_LENGTH_BYTES).also { SecureRandom().nextBytes(it) }
|
||||
val sessionId = generateSessionId(publicKeyBase64, secretKeyBytes)
|
||||
secretKeyBytes to sessionId
|
||||
}
|
||||
|
||||
suspend fun decryptSecret(base64Secret: String, base64Iv: String, secretKeyHex: String): String =
|
||||
suspend fun encryptPin(pin: String, secretKeyBytes: ByteArray): EncryptedData = withContext(dispatchers.default) {
|
||||
val bytes = pinBlockByteArray(pin)
|
||||
try {
|
||||
encryptSecret(bytes, secretKeyBytes)
|
||||
} finally {
|
||||
bytes.clear()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decryptSecret(base64Secret: String, base64Iv: String, secretKeyBytes: ByteArray): String =
|
||||
withContext(dispatchers.default) {
|
||||
val cipherTextBytes = Base64.decode(base64Secret, Base64.NO_WRAP)
|
||||
if (cipherTextBytes.size < TAG_LENGTH_BYTES) error("Cipher text too short")
|
||||
|
||||
val initializationVectorBytes = Base64.decode(base64Iv, Base64.NO_WRAP)
|
||||
val secretKeyBytes = secretKeyHex.hexToBytes()
|
||||
|
||||
val aesSecretKey = SecretKeySpec(secretKeyBytes, AES_ALGORITHM)
|
||||
val gcmParameterSpec = GCMParameterSpec(TAG_LENGTH_BYTES * BITS_PER_BYTE, initializationVectorBytes)
|
||||
|
||||
|
|
@ -51,8 +62,25 @@ internal class RainCryptoUtil @Inject constructor(
|
|||
plaintextBytes.toString(StandardCharsets.UTF_8).trim().ifEmpty { error("Invalid decrypted data") }
|
||||
}
|
||||
|
||||
private fun generateSessionId(publicKeyBase64: String, secretKeyHex: String): String {
|
||||
val secretKeyBytes = secretKeyHex.hexToBytes()
|
||||
private fun encryptSecret(bytes: ByteArray, secretKeyBytes: ByteArray): EncryptedData {
|
||||
val iv = ByteArray(IV_LENGTH_BYTES).also { SecureRandom().nextBytes(it) }
|
||||
|
||||
val aesSecretKey = SecretKeySpec(secretKeyBytes, AES_ALGORITHM)
|
||||
val gcmSpec = GCMParameterSpec(TAG_LENGTH_BYTES * BITS_PER_BYTE, iv)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, aesSecretKey, gcmSpec)
|
||||
|
||||
val ciphertext = cipher.doFinal(bytes)
|
||||
bytes.clear()
|
||||
|
||||
return EncryptedData(
|
||||
encryptedBase64 = Base64.encodeToString(ciphertext, Base64.NO_WRAP),
|
||||
ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateSessionId(publicKeyBase64: String, secretKeyBytes: ByteArray): String {
|
||||
val publicKeyDerBytes = Base64.decode(publicKeyBase64, Base64.NO_WRAP)
|
||||
val publicKeySpec = X509EncodedKeySpec(publicKeyDerBytes)
|
||||
val rsaPublicKey = KeyFactory.getInstance(RSA_ALGORITHM).generatePublic(publicKeySpec)
|
||||
|
|
@ -66,4 +94,26 @@ internal class RainCryptoUtil @Inject constructor(
|
|||
|
||||
return Base64.encodeToString(cipherTextBytes, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats PIN into a PIN block using schema: [Prefix][Length][PIN][fill with F].
|
||||
* Example: 246784FFFFFFFFFF for PIN 6784.
|
||||
*/
|
||||
private fun pinBlockByteArray(pin: String): ByteArray {
|
||||
require(pin.length == PIN_LENGTH) { "PIN length must be $PIN_LENGTH" }
|
||||
require(pin.all { it.isDigit() }) { "PIN must contain digits only" }
|
||||
|
||||
val pinBlockHexLength = PIN_LENGTH_BYTES * 2
|
||||
val hex = buildString(pinBlockHexLength) {
|
||||
append(PIN_BLOCK_ISO_9564_FORMAT_PREFIX)
|
||||
append(PIN_LENGTH.toString())
|
||||
append(pin)
|
||||
while (length < pinBlockHexLength) append(PIN_BLOCK_FILL_CHAR)
|
||||
}
|
||||
return hex.toByteArray(StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun ByteArray.clear() {
|
||||
for (i in indices) this[i] = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,25 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
import timber.log.Timber
|
||||
import java.util.Currency
|
||||
|
||||
internal object TangemPayTxHistoryItemConverter :
|
||||
internal class TangemPayTxHistoryItemConverter(moshi: Moshi) :
|
||||
Converter<TangemPayTxHistoryResponse.Transaction, TangemPayTxHistoryItem?> {
|
||||
|
||||
private val spendAdapter by lazy { moshi.adapter(TangemPayTxHistoryResponse.Spend::class.java) }
|
||||
private val paymentAdapter by lazy { moshi.adapter(TangemPayTxHistoryResponse.Payment::class.java) }
|
||||
private val feeAdapter by lazy { moshi.adapter(TangemPayTxHistoryResponse.Fee::class.java) }
|
||||
private val collateralAdapter by lazy { moshi.adapter(TangemPayTxHistoryResponse.Collateral::class.java) }
|
||||
|
||||
override fun convert(value: TangemPayTxHistoryResponse.Transaction): TangemPayTxHistoryItem? {
|
||||
return value.spend?.let { convertSpend(id = value.id, spend = it) }
|
||||
?: value.payment?.let { convertPayment(id = value.id, payment = it) }
|
||||
?: value.fee?.let { convertFee(id = value.id, fee = it) }
|
||||
?: value.collateral?.let { convertCollateral(id = value.id, collateral = it) }
|
||||
?: run {
|
||||
Timber.wtf("unknown type of transaction: $value")
|
||||
null
|
||||
|
|
@ -22,8 +29,8 @@ internal object TangemPayTxHistoryItemConverter :
|
|||
private fun convertSpend(id: String, spend: TangemPayTxHistoryResponse.Spend): TangemPayTxHistoryItem.Spend {
|
||||
return TangemPayTxHistoryItem.Spend(
|
||||
id = id,
|
||||
// If postedAt is null, it means transaction wasn't posted and was likely declined. Use authorizedAt
|
||||
date = spend.postedAt ?: spend.authorizedAt,
|
||||
jsonRepresentation = spendAdapter.toJson(spend),
|
||||
date = spend.authorizedAt,
|
||||
amount = spend.amount,
|
||||
currency = Currency.getInstance(spend.currency),
|
||||
enrichedMerchantName = spend.enrichedMerchantName,
|
||||
|
|
@ -41,18 +48,39 @@ internal object TangemPayTxHistoryItemConverter :
|
|||
): TangemPayTxHistoryItem.Payment {
|
||||
return TangemPayTxHistoryItem.Payment(
|
||||
id = id,
|
||||
jsonRepresentation = paymentAdapter.toJson(payment),
|
||||
date = payment.postedAt,
|
||||
currency = Currency.getInstance(payment.currency),
|
||||
amount = payment.amount,
|
||||
transactionHash = payment.transactionHash,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertFee(id: String, fee: TangemPayTxHistoryResponse.Fee): TangemPayTxHistoryItem.Fee {
|
||||
return TangemPayTxHistoryItem.Fee(
|
||||
id = id,
|
||||
jsonRepresentation = feeAdapter.toJson(fee),
|
||||
date = fee.postedAt,
|
||||
currency = Currency.getInstance(fee.currency),
|
||||
amount = fee.amount,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertCollateral(
|
||||
id: String,
|
||||
collateral: TangemPayTxHistoryResponse.Collateral,
|
||||
): TangemPayTxHistoryItem.Collateral? {
|
||||
val date = collateral.postedAt ?: return run {
|
||||
Timber.e("Collateral transaction postedAt is null: $collateral")
|
||||
return@run null
|
||||
}
|
||||
return TangemPayTxHistoryItem.Collateral(
|
||||
id = id,
|
||||
jsonRepresentation = collateralAdapter.toJson(collateral),
|
||||
date = date,
|
||||
currency = Currency.getInstance("usd"),
|
||||
amount = collateral.amount,
|
||||
transactionHash = collateral.transactionHash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ android {
|
|||
dependencies {
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.account.status)
|
||||
implementation(projects.domain.walletConnect)
|
||||
implementation(projects.domain.walletConnect.models)
|
||||
implementation(projects.domain.transaction)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ import com.tangem.data.walletconnect.utils.WcScope
|
|||
import com.tangem.datasource.di.SdkMoshi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.datasource.local.walletconnect.WalletConnectStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.walletconnect.WcPairService
|
||||
import com.tangem.domain.walletconnect.WcRequestService
|
||||
|
|
@ -92,6 +96,7 @@ internal object WalletConnectDataModule {
|
|||
getWallets: GetWalletsUseCase,
|
||||
wcNetworksConverter: WcNetworksConverter,
|
||||
analytics: AnalyticsEventHandler,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
wcScope: WcScope,
|
||||
): DefaultWcSessionsManager {
|
||||
return DefaultWcSessionsManager(
|
||||
|
|
@ -100,6 +105,7 @@ internal object WalletConnectDataModule {
|
|||
getWallets = getWallets,
|
||||
wcNetworksConverter = wcNetworksConverter,
|
||||
analytics = analytics,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
scope = wcScope,
|
||||
)
|
||||
}
|
||||
|
|
@ -179,22 +185,26 @@ internal object WalletConnectDataModule {
|
|||
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
singleAccountSupplier: SingleAccountSupplier,
|
||||
): WcNetworksConverter = WcNetworksConverter(
|
||||
namespaceConverters = namespaceConverters,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
singleAccountSupplier = singleAccountSupplier,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun associateNetworksDelegate(
|
||||
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
|
||||
networksConverter: WcNetworksConverter,
|
||||
multiAccountListSupplier: MultiAccountListSupplier,
|
||||
getWallets: GetWalletsUseCase,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
): AssociateNetworksDelegate = AssociateNetworksDelegate(
|
||||
namespaceConverters = namespaceConverters,
|
||||
getWallets = getWallets,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
networksConverter = networksConverter,
|
||||
multiAccountListSupplier = multiAccountListSupplier,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -2,22 +2,25 @@ package com.tangem.data.walletconnect.pair
|
|||
|
||||
import com.reown.walletkit.client.Wallet
|
||||
import com.reown.walletkit.client.Wallet.Model.Namespace
|
||||
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.data.walletconnect.utils.WcNetworksConverter
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.walletconnect.model.WcPairError
|
||||
import com.tangem.domain.walletconnect.model.WcSessionProposal.ProposalNetwork
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
internal class AssociateNetworksDelegate(
|
||||
private val namespaceConverters: Set<WcNamespaceConverter>,
|
||||
private val networksConverter: WcNetworksConverter,
|
||||
private val multiAccountListSupplier: MultiAccountListSupplier,
|
||||
private val getWallets: GetWalletsUseCase,
|
||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
) {
|
||||
|
||||
@Throws(WcPairError.UnsupportedBlockchains::class)
|
||||
|
|
@ -29,18 +32,48 @@ internal class AssociateNetworksDelegate(
|
|||
.subtract(requiredNamespaces)
|
||||
|
||||
return userWallets.associateWith { wallet ->
|
||||
mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces, sessionProposal)
|
||||
mapNetworksForPortfolio(wallet, null, requiredNamespaces, optionalNamespaces, sessionProposal)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(WcPairError.UnsupportedBlockchains::class)
|
||||
suspend fun associateAccounts(sessionProposal: Wallet.Model.SessionProposal): Map<AccountId, ProposalNetwork> {
|
||||
val userWallets = getWallets.invokeSync()
|
||||
.filter { it.isMultiCurrency && !it.isLocked }
|
||||
val requiredNamespaces: Set<String> = sessionProposal.requiredNamespaces.setOfChainId()
|
||||
val optionalNamespaces: Set<String> = sessionProposal.optionalNamespaces.setOfChainId()
|
||||
// remove duplicates
|
||||
.subtract(requiredNamespaces)
|
||||
val allAccounts = multiAccountListSupplier.invoke()
|
||||
.filter { it.isNotEmpty() }
|
||||
.first()
|
||||
.filter { accountList -> userWallets.any { accountList.userWalletId == it.walletId } }
|
||||
.map { accountList -> accountList to userWallets.first { it.walletId == accountList.userWalletId } }
|
||||
|
||||
return allAccounts
|
||||
.map { (accountList, wallet) -> accountList.accounts.map { account -> account to wallet } }
|
||||
.flatten()
|
||||
.associate { (account, wallet) ->
|
||||
account.accountId to mapNetworksForPortfolio(
|
||||
wallet = wallet,
|
||||
account = account,
|
||||
requiredNamespaces = requiredNamespaces,
|
||||
optionalNamespaces = optionalNamespaces,
|
||||
sessionProposal = sessionProposal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
private suspend fun mapNetworksForWallet(
|
||||
private suspend fun mapNetworksForPortfolio(
|
||||
wallet: UserWallet,
|
||||
account: Account?,
|
||||
requiredNamespaces: Set<String>,
|
||||
optionalNamespaces: Set<String>,
|
||||
sessionProposal: Wallet.Model.SessionProposal,
|
||||
): ProposalNetwork {
|
||||
val walletNetworks = getWalletNetworks(userWalletId = wallet.walletId)
|
||||
val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) }
|
||||
?: getWalletNetworks(userWalletId = wallet.walletId)
|
||||
|
||||
val unknownRequired = mutableSetOf<String>()
|
||||
val unknownOptional = mutableSetOf<String>()
|
||||
|
|
@ -50,28 +83,28 @@ internal class AssociateNetworksDelegate(
|
|||
val notAdded = mutableSetOf<Network>()
|
||||
|
||||
requiredNamespaces.forEach { chainId ->
|
||||
val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) }
|
||||
val wcNetwork = networksConverter.createNetwork(chainId, wallet)
|
||||
if (wcNetwork == null) {
|
||||
unknownRequired.add(missingNetworkName(chainId))
|
||||
return@forEach
|
||||
}
|
||||
val walletNetwork = walletNetworks.find { network -> wcNetwork.rawId == network.rawId }
|
||||
val portfolioNetwork = portfolioNetworks.find { network -> wcNetwork.rawId == network.rawId }
|
||||
|
||||
if (walletNetwork == null) {
|
||||
if (portfolioNetwork == null) {
|
||||
missingRequired.add(wcNetwork)
|
||||
} else {
|
||||
required.add(walletNetwork)
|
||||
required.add(portfolioNetwork)
|
||||
}
|
||||
}
|
||||
optionalNamespaces.forEach { chainId ->
|
||||
val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) }
|
||||
val wcNetwork = networksConverter.createNetwork(chainId, wallet)
|
||||
if (wcNetwork == null) {
|
||||
unknownOptional.add(missingNetworkName(chainId))
|
||||
return@forEach
|
||||
}
|
||||
val walletNetwork = walletNetworks.find { network -> wcNetwork.rawId == network.rawId }
|
||||
if (walletNetwork != null) {
|
||||
available.add(walletNetwork)
|
||||
val portfolioNetwork = portfolioNetworks.find { network -> wcNetwork.rawId == network.rawId }
|
||||
if (portfolioNetwork != null) {
|
||||
available.add(portfolioNetwork)
|
||||
} else {
|
||||
notAdded.add(wcNetwork)
|
||||
}
|
||||
|
|
@ -88,16 +121,18 @@ internal class AssociateNetworksDelegate(
|
|||
required = required,
|
||||
available = available,
|
||||
notAdded = notAdded,
|
||||
account = account,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getWalletNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
|
||||
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
.orEmpty()
|
||||
.filterIsInstance<CryptoCurrency.Coin>()
|
||||
.map(CryptoCurrency.Coin::network)
|
||||
return networksConverter.getWalletNetworks(userWalletId)
|
||||
// flatten all derivation
|
||||
.distinctBy { it.rawId }
|
||||
}
|
||||
|
||||
private suspend fun getAccountNetworks(accountId: AccountId): List<Network> {
|
||||
return networksConverter.getAccountNetworks(accountId)
|
||||
// flatten all derivation
|
||||
.distinctBy { it.rawId }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.reown.walletkit.client.Wallet
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.data.walletconnect.utils.WC_TAG
|
||||
import com.tangem.data.walletconnect.utils.getDappOriginUrl
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.blockaid.BlockAidVerifier
|
||||
import com.tangem.domain.walletconnect.WcAnalyticEvents
|
||||
import com.tangem.domain.walletconnect.model.*
|
||||
|
|
@ -34,6 +35,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
private val sdkDelegate: WcPairSdkDelegate,
|
||||
private val blockAidVerifier: BlockAidVerifier,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
@Assisted private val pairRequest: WcPairRequest,
|
||||
) : WcPairUseCase {
|
||||
|
||||
|
|
@ -111,6 +113,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
val sessionDTO = WcSessionDTO(
|
||||
topic = "",
|
||||
walletId = sessionForApprove.wallet.walletId,
|
||||
accountId = sessionForApprove.account?.accountId,
|
||||
url = sdkVerifyContext.getDappOriginUrl(),
|
||||
securityStatus = proposalState.dAppSession.securityStatus,
|
||||
connectingTime = connectingTime,
|
||||
|
|
@ -196,6 +199,11 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
verifyContext: Wallet.Model.VerifyContext,
|
||||
): Either<WcPairError, WcPairState.Proposal> = runCatching {
|
||||
val proposalNetwork = associateNetworksDelegate.associate(sessionProposal)
|
||||
val proposalAccountNetwork = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
associateNetworksDelegate.associateAccounts(sessionProposal)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val verificationInfo = when {
|
||||
verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE
|
||||
verifyContext.isScam == true -> CheckDAppResult.UNSAFE
|
||||
|
|
@ -204,7 +212,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
CheckDAppResult.FAILED_TO_VERIFY
|
||||
}
|
||||
}
|
||||
val requestedNetworks = proposalNetwork
|
||||
val requestedNetworks = (proposalAccountNetwork ?: proposalNetwork)
|
||||
.values.map { it.available.plus(it.required) }.flatten().toSet()
|
||||
analytics.send(
|
||||
WcAnalyticEvents.PairRequested(
|
||||
|
|
@ -225,6 +233,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
dAppMetaData = appMetaData,
|
||||
proposalNetwork = proposalNetwork,
|
||||
securityStatus = verificationInfo,
|
||||
proposalAccountNetwork = proposalAccountNetwork,
|
||||
)
|
||||
WcPairState.Proposal(dAppSession)
|
||||
}.fold(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import com.reown.walletkit.client.WalletKit
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.data.walletconnect.utils.*
|
||||
import com.tangem.datasource.local.walletconnect.WalletConnectStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.walletconnect.WcAnalyticEvents
|
||||
import com.tangem.domain.walletconnect.model.WcSession
|
||||
|
|
@ -31,16 +33,20 @@ internal class DefaultWcSessionsManager(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val wcNetworksConverter: WcNetworksConverter,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val scope: WcScope,
|
||||
) : WcSessionsManager, WcSdkObserver {
|
||||
|
||||
private val onSessionDelete = Channel<Wallet.Model.SessionDelete>(capacity = Channel.BUFFERED)
|
||||
private val oneTimeMigration = MutableStateFlow(false)
|
||||
|
||||
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()
|
||||
val someMigrate = migrateToAccountSession(inStore)
|
||||
if (someMigrate) return@transform
|
||||
val associatedSessions: List<WcSession> = associate(inSdk, inStore, wallets)
|
||||
val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions)
|
||||
if (someRemove) return@transform // ignore emit, wait next one
|
||||
|
|
@ -49,6 +55,26 @@ internal class DefaultWcSessionsManager(
|
|||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
private suspend fun migrateToAccountSession(inStore: Set<WcSessionDTO>): Boolean {
|
||||
if (!accountsFeatureToggles.isFeatureEnabled) return false
|
||||
if (oneTimeMigration.value) return false
|
||||
|
||||
var someMigrated = false
|
||||
|
||||
val updatedSessions = inStore.mapTo(mutableSetOf()) { sessionDTO ->
|
||||
if (sessionDTO.accountId == null) {
|
||||
someMigrated = true
|
||||
val mainAccountId = AccountId.forMainCryptoPortfolio(sessionDTO.walletId)
|
||||
sessionDTO.copy(accountId = mainAccountId)
|
||||
} else {
|
||||
sessionDTO
|
||||
}
|
||||
}
|
||||
if (someMigrated) store.saveSessions(updatedSessions)
|
||||
oneTimeMigration.value = true
|
||||
return someMigrated
|
||||
}
|
||||
|
||||
override fun onWcSdkInit() {
|
||||
listenOnSessionDelete()
|
||||
extendSessions()
|
||||
|
|
@ -90,7 +116,11 @@ internal class DefaultWcSessionsManager(
|
|||
val wcSessions = savedPending.plus(inStore).mapNotNull { storeSession ->
|
||||
val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null
|
||||
val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null
|
||||
val networks = wcNetworksConverter.findWalletNetworks(wallet, sdkSession)
|
||||
val account = storeSession.accountId?.let { wcNetworksConverter.getAccount(it) }
|
||||
if (accountsFeatureToggles.isFeatureEnabled && account == null) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val networks = wcNetworksConverter.findWalletNetworks(wallet, account, sdkSession)
|
||||
val originUrl = storeSession.url ?: sdkSession.metaData?.url ?: ""
|
||||
WcSession(
|
||||
wallet = wallet,
|
||||
|
|
@ -102,6 +132,7 @@ internal class DefaultWcSessionsManager(
|
|||
),
|
||||
securityStatus = storeSession.securityStatus,
|
||||
networks = networks,
|
||||
account = account,
|
||||
connectingTime = storeSession.connectingTime,
|
||||
showWalletInfo = wallets.size > 1,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@ package com.tangem.data.walletconnect.utils
|
|||
import com.reown.walletkit.client.Wallet
|
||||
import com.tangem.data.common.currency.isCustomCoin
|
||||
import com.tangem.data.walletconnect.model.CAIP10
|
||||
import com.tangem.domain.account.producer.SingleAccountProducer
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -18,6 +25,8 @@ import javax.inject.Inject
|
|||
internal class WcNetworksConverter @Inject constructor(
|
||||
private val namespaceConverters: Set<WcNamespaceConverter>,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val singleAccountSupplier: SingleAccountSupplier,
|
||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
) {
|
||||
|
||||
|
|
@ -67,8 +76,13 @@ internal class WcNetworksConverter @Inject constructor(
|
|||
return allCoinNetwork
|
||||
}
|
||||
|
||||
suspend fun findWalletNetworks(wallet: UserWallet, sdkSession: Wallet.Model.Session): Set<Network> {
|
||||
val walletNetworks = getWalletNetworks(wallet.walletId)
|
||||
suspend fun findWalletNetworks(
|
||||
wallet: UserWallet,
|
||||
account: Account?,
|
||||
sdkSession: Wallet.Model.Session,
|
||||
): Set<Network> {
|
||||
val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) }
|
||||
?: getWalletNetworks(wallet.walletId)
|
||||
val existNetworks = sdkSession.namespaces.values
|
||||
.map { it.accounts }.flatten().toSet()
|
||||
.mapNotNull { CAIP10.fromRaw(it) }
|
||||
|
|
@ -76,7 +90,7 @@ internal class WcNetworksConverter @Inject constructor(
|
|||
val blockchain = namespaceConverters
|
||||
.firstNotNullOfOrNull { it.toBlockchain(caip10.chainId) }
|
||||
?: return@mapNotNullTo null
|
||||
walletNetworks
|
||||
portfolioNetworks
|
||||
// find all derivation
|
||||
.filter { it.rawId == blockchain.id }
|
||||
// find equal address
|
||||
|
|
@ -89,18 +103,38 @@ internal class WcNetworksConverter @Inject constructor(
|
|||
return existNetworks
|
||||
}
|
||||
|
||||
suspend fun getAccount(accountId: AccountId): Account? {
|
||||
return singleAccountSupplier.getSyncOrNull(SingleAccountProducer.Params(accountId))
|
||||
}
|
||||
|
||||
suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List<Network> {
|
||||
val walletNetworks = getWalletNetworks(sessionForApprove.wallet.walletId)
|
||||
val portfolioNetworks = sessionForApprove.account?.let { getAccountNetworks(it.accountId) }
|
||||
?: getWalletNetworks(sessionForApprove.wallet.walletId)
|
||||
return sessionForApprove.network
|
||||
.map { network -> walletNetworks.filter { walletNetwork -> walletNetwork.rawId == network.rawId } }
|
||||
.map { network -> portfolioNetworks.filter { walletNetwork -> walletNetwork.rawId == network.rawId } }
|
||||
.flatten()
|
||||
}
|
||||
|
||||
private suspend fun getWalletNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
suspend fun getWalletNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
|
||||
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
.orEmpty()
|
||||
.filterIsInstance<CryptoCurrency.Coin>().map(CryptoCurrency.Coin::network)
|
||||
}
|
||||
|
||||
private suspend fun getAccountStatus(accountId: AccountId): AccountStatus? {
|
||||
return singleAccountStatusListSupplier.getSyncOrNull(
|
||||
SingleAccountStatusListProducer.Params(accountId.userWalletId),
|
||||
)?.accountStatuses?.find { it.account.accountId == accountId }
|
||||
}
|
||||
|
||||
suspend fun getAccountNetworks(accountId: AccountId): List<Network> {
|
||||
return getAccountStatus(accountId)
|
||||
?.flattenCurrencies()
|
||||
?.map { it.currency }
|
||||
?.filterIsInstance<CryptoCurrency.Coin>()
|
||||
?.map(CryptoCurrency.Coin::network)
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
|
|
@ -14,9 +14,13 @@ import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate
|
|||
import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase
|
||||
import com.tangem.data.walletconnect.pair.WcPairSdkDelegate
|
||||
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.blockaid.BlockAidVerifier
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletconnect.model.*
|
||||
import com.tangem.domain.walletconnect.model.WcPairError
|
||||
import com.tangem.domain.walletconnect.model.WcPairRequest
|
||||
import com.tangem.domain.walletconnect.model.WcSession
|
||||
import com.tangem.domain.walletconnect.model.WcSessionApprove
|
||||
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerifyOrder
|
||||
|
|
@ -33,6 +37,7 @@ internal class DefaultWcPairUseCaseTest {
|
|||
private val analytics: AnalyticsEventHandler = mockk<AnalyticsEventHandler>(relaxed = true)
|
||||
private val sdkDelegate: WcPairSdkDelegate = mockk<WcPairSdkDelegate>()
|
||||
private val blockAidVerifier: BlockAidVerifier = mockk<BlockAidVerifier>()
|
||||
private val accountsFeatureToggles = mockk<AccountsFeatureToggles>()
|
||||
|
||||
private val url = "testUrl"
|
||||
private val source = WcPairRequest.Source.QR
|
||||
|
|
@ -70,6 +75,7 @@ internal class DefaultWcPairUseCaseTest {
|
|||
get() = WcSessionApprove(
|
||||
wallet = MockUserWalletFactory.create(),
|
||||
network = listOf(),
|
||||
account = null,
|
||||
)
|
||||
|
||||
private val sdkApprove: Wallet.Params.SessionApprove
|
||||
|
|
@ -102,6 +108,7 @@ internal class DefaultWcPairUseCaseTest {
|
|||
networks = setOf(),
|
||||
connectingTime = null,
|
||||
showWalletInfo = false,
|
||||
account = null,
|
||||
)
|
||||
|
||||
private fun useCaseFactory() = DefaultWcPairUseCase(
|
||||
|
|
@ -110,12 +117,14 @@ internal class DefaultWcPairUseCaseTest {
|
|||
sdkDelegate = sdkDelegate,
|
||||
blockAidVerifier = blockAidVerifier,
|
||||
analytics = analytics,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source),
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
coEvery { associateNetworksDelegate.associate(sdkProposal) } returns mapOf()
|
||||
coEvery { accountsFeatureToggles.isFeatureEnabled } returns false
|
||||
coEvery {
|
||||
caipNamespaceDelegate.associate(
|
||||
sessionProposal = sdkProposal,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ internal class WcSignUseCaseDelegateTest {
|
|||
session = WcSession(
|
||||
wallet = MockUserWalletFactory.create(),
|
||||
networks = setOf(),
|
||||
account = null,
|
||||
securityStatus = CheckDAppResult.FAILED_TO_VERIFY,
|
||||
connectingTime = 0L,
|
||||
sdkModel = WcSdkSession(
|
||||
|
|
|
|||
|
|
@ -10,20 +10,13 @@ import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
|
|||
import com.tangem.datasource.api.common.response.fold
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.MarkUserWalletWasCreatedBody
|
||||
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody
|
||||
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO
|
||||
import com.tangem.datasource.api.tangemTech.models.*
|
||||
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletBody
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFICATION_SHOW_TIME
|
||||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.datasource.local.preferences.utils.*
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -37,7 +30,6 @@ 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>
|
||||
|
||||
|
|
@ -52,7 +44,7 @@ internal class DefaultWalletsRepository(
|
|||
) : WalletsRepository {
|
||||
|
||||
private val upgradeWalletNotificationDisabled: MutableStateFlow<Set<UserWalletId>> =
|
||||
MutableStateFlow(mutableSetOf<UserWalletId>())
|
||||
MutableStateFlow(mutableSetOf())
|
||||
|
||||
override suspend fun shouldSaveUserWalletsSync(): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
|
||||
|
|
@ -260,6 +252,14 @@ internal class DefaultWalletsRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun createWallet(userWalletId: UserWalletId) {
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.createWallet(
|
||||
body = OnlyWalletIdBody(userWalletId.stringValue),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) {
|
||||
runCatching(dispatchers.io) {
|
||||
tangemTechApi.updateSeedPhraseSecondNotificationStatus(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue