Updated on 2026-08-14
This commit is contained in:
commit
86d72b171f
657 changed files with 19875 additions and 4004 deletions
|
|
@ -12,6 +12,10 @@ android {
|
|||
namespace = "com.tangem.data.tokens"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region Project - Data
|
||||
|
|
@ -62,4 +66,13 @@ dependencies {
|
|||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.data.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
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.DemoConfig
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiWalletCryptoCurrenciesFetcher]
|
||||
*
|
||||
* @property userWalletsStore [UserWallet]'s store
|
||||
* @property tangemTechApi Tangem Tech API
|
||||
* @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 dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultMultiWalletCryptoCurrenciesFetcher(
|
||||
private val demoConfig: DemoConfig,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val customTokensMerger: CustomTokensMerger,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
private val expressServiceLoader: ExpressServiceLoader,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiWalletCryptoCurrenciesFetcher {
|
||||
|
||||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
|
||||
override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
|
||||
if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet")
|
||||
|
||||
val response = if (userWallet is UserWallet.Cold && userWallet.isDemoWalletWithoutSavedTokens()) {
|
||||
createDefaultUserTokensResponse(scanResponse = userWallet.scanResponse)
|
||||
} else {
|
||||
safeApiCall(
|
||||
call = {
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.getUserTokens(userId = userWallet.walletId.stringValue).bind()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
handleFetchTokensError(error = it, userWallet = userWallet.requireColdWallet()) // TODO 11142
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val compatibleUserTokensResponse = response
|
||||
.let { it.copy(tokens = it.tokens.distinct()) }
|
||||
.let { customTokensMerger.mergeIfPresented(userWalletId = userWallet.walletId, response = it) }
|
||||
|
||||
userTokensSaver.store(userWalletId = userWallet.walletId, response = compatibleUserTokensResponse)
|
||||
|
||||
fetchExpressAssetsByNetworkIds(userWallet = userWallet, userTokens = compatibleUserTokensResponse)
|
||||
}
|
||||
|
||||
private suspend fun UserWallet.Cold.isDemoWalletWithoutSavedTokens(): Boolean {
|
||||
val isDemoCard = demoConfig.isDemoCardId(cardId = cardId)
|
||||
|
||||
return if (isDemoCard) {
|
||||
val response = userTokensResponseStore.getSyncOrNull(userWalletId = walletId)
|
||||
|
||||
response == null
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleFetchTokensError(error: ApiResponseError, userWallet: UserWallet): UserTokensResponse {
|
||||
val userWalletId = userWallet.walletId
|
||||
|
||||
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
|
||||
?: createDefaultUserTokensResponse(scanResponse = userWallet.requireColdWallet().scanResponse)
|
||||
|
||||
if (error is ApiResponseError.HttpException && error.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
|
||||
Timber.w(error, "Requested currencies could not be found in the remote store for: $userWalletId")
|
||||
|
||||
userTokensSaver.push(userWalletId, response)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
expressServiceLoader.update(userWallet = userWallet, userTokens = tokens)
|
||||
}
|
||||
|
||||
private fun createDefaultUserTokensResponse(scanResponse: ScanResponse): UserTokensResponse {
|
||||
return userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.data.tokens
|
||||
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiWalletCryptoCurrenciesProducer]
|
||||
*
|
||||
* @property params params
|
||||
* @property userWalletsStore UserWallet's store
|
||||
* @property userTokensResponseStore store of `UserTokensResponse`
|
||||
* @property responseCryptoCurrenciesFactory factory for creating [CryptoCurrency] from `UserTokensResponse`
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constructor(
|
||||
@Assisted val params: MultiWalletCryptoCurrenciesProducer.Params,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiWalletCryptoCurrenciesProducer {
|
||||
|
||||
override val fallback: Set<CryptoCurrency>
|
||||
get() = emptySet()
|
||||
|
||||
override fun produce(): Flow<Set<CryptoCurrency>> {
|
||||
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId).requireColdWallet() // TODO [REDACTED_TASK_KEY]
|
||||
|
||||
if (!userWallet.isMultiCurrency) {
|
||||
error("${this::class.simpleName} supports only multi-currency wallet")
|
||||
}
|
||||
|
||||
return userTokensResponseStore.get(userWalletId = params.userWalletId)
|
||||
.distinctUntilChanged()
|
||||
.map { response ->
|
||||
if (response == null) return@map emptySet()
|
||||
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = response,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
).toSet()
|
||||
}
|
||||
.onEmpty { emit(fallback) }
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MultiWalletCryptoCurrenciesProducer.Factory {
|
||||
override fun create(
|
||||
params: MultiWalletCryptoCurrenciesProducer.Params,
|
||||
): DefaultMultiWalletCryptoCurrenciesProducer
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.data.tokens.di
|
||||
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
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.demo.DemoConfig
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal class MultiWalletCryptoCurrenciesFetcherModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideMultiWalletCryptoCurrenciesFetcher(
|
||||
tangemTechApi: TangemTechApi,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
userTokensResponseStore: UserTokensResponseStore,
|
||||
userTokensSaver: UserTokensSaver,
|
||||
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
expressServiceLoader: ExpressServiceLoader,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): MultiWalletCryptoCurrenciesFetcher {
|
||||
return DefaultMultiWalletCryptoCurrenciesFetcher(
|
||||
demoConfig = DemoConfig(),
|
||||
userWalletsStore = userWalletsStore,
|
||||
tangemTechApi = tangemTechApi,
|
||||
customTokensMerger = CustomTokensMerger(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userTokensSaver = userTokensSaver,
|
||||
dispatchers = dispatchers,
|
||||
),
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.data.tokens.di
|
||||
|
||||
import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface MultiWalletCryptoCurrenciesProducerModule {
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun bindMultiWalletCryptoCurrenciesProducerFactory(
|
||||
impl: DefaultMultiWalletCryptoCurrenciesProducer.Factory,
|
||||
): MultiWalletCryptoCurrenciesProducer.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.data.tokens.di
|
||||
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal class MultiWalletCryptoCurrenciesSupplierModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideMultiWalletCryptoCurrenciesSupplier(
|
||||
factory: MultiWalletCryptoCurrenciesProducer.Factory,
|
||||
): MultiWalletCryptoCurrenciesSupplier {
|
||||
return object : MultiWalletCryptoCurrenciesSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { "multi_crypto_currency_${it.userWalletId}" },
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.tokens.di
|
|||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
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
|
||||
|
|
@ -10,6 +11,7 @@ import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckReposi
|
|||
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.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
|
|
@ -30,7 +32,7 @@ internal object TokensDataModule {
|
|||
@Singleton
|
||||
fun provideCurrenciesRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
userTokensResponseStore: UserTokensResponseStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
cacheRegistry: CacheRegistry,
|
||||
|
|
@ -39,18 +41,20 @@ internal object TokensDataModule {
|
|||
excludedBlockchains: ExcludedBlockchains,
|
||||
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
tokensSaver: UserTokensSaver,
|
||||
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
): CurrenciesRepository {
|
||||
return DefaultCurrenciesRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userWalletsStore = userWalletsStore,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
cacheRegistry = cacheRegistry,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
dispatchers = dispatchers,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
userTokensSaver = tokensSaver,
|
||||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,9 @@ 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.preferences.AppPreferencesStore
|
||||
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.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
|
|
@ -41,16 +39,16 @@ internal class DefaultCurrenciesRepository(
|
|||
private val userWalletsStore: UserWalletsStore,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val expressServiceLoader: ExpressServiceLoader,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
) : CurrenciesRepository {
|
||||
|
||||
private val demoConfig = DemoConfig()
|
||||
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
|
||||
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
|
||||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
private val customTokensMerger = CustomTokensMerger(
|
||||
|
|
@ -92,7 +90,10 @@ internal class DefaultCurrenciesRepository(
|
|||
response = updatedResponse,
|
||||
)
|
||||
|
||||
fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse)
|
||||
fetchExpressAssetsByNetworkIds(
|
||||
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
|
||||
userTokens = updatedResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +116,11 @@ internal class DefaultCurrenciesRepository(
|
|||
userWalletId = userWalletId,
|
||||
response = updatedResponse,
|
||||
)
|
||||
fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse)
|
||||
|
||||
fetchExpressAssetsByNetworkIds(
|
||||
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
|
||||
userTokens = updatedResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +206,7 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
|
||||
return channelFlow {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
|
||||
if (userWallet.isMultiCurrency) {
|
||||
getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send)
|
||||
|
|
@ -218,13 +223,19 @@ internal class DefaultCurrenciesRepository(
|
|||
refresh: Boolean,
|
||||
): CryptoCurrency {
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
|
||||
|
||||
val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(
|
||||
userWallet.requireColdWallet().scanResponse,
|
||||
)
|
||||
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency), refresh)
|
||||
|
||||
fetchExpressAssetsByNetworkIds(
|
||||
userWallet = userWallet,
|
||||
cryptoCurrencies = listOf(currency),
|
||||
refresh = refresh,
|
||||
)
|
||||
|
||||
currency
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +245,8 @@ internal class DefaultCurrenciesRepository(
|
|||
refresh: Boolean,
|
||||
): List<CryptoCurrency> {
|
||||
return withContext(dispatchers.io) {
|
||||
val scanResponse = getUserWallet(userWalletId).requireColdWallet().scanResponse
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
val scanResponse = userWallet.requireColdWallet().scanResponse
|
||||
|
||||
val currencies = if (scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
|
||||
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = scanResponse)
|
||||
|
|
@ -244,7 +256,12 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
}
|
||||
|
||||
fetchExpressAssetsByNetworkIds(userWalletId, currencies, refresh)
|
||||
fetchExpressAssetsByNetworkIds(
|
||||
userWallet = userWallet,
|
||||
cryptoCurrencies = currencies,
|
||||
refresh = refresh,
|
||||
)
|
||||
|
||||
currencies
|
||||
}
|
||||
}
|
||||
|
|
@ -254,7 +271,7 @@ internal class DefaultCurrenciesRepository(
|
|||
id: CryptoCurrency.ID,
|
||||
): CryptoCurrency {
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
|
||||
|
||||
val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
|
||||
|
|
@ -262,14 +279,14 @@ internal class DefaultCurrenciesRepository(
|
|||
)
|
||||
.find { it.id == id }
|
||||
requireNotNull(currency) { "Unable to find currency with provided ID: $id" }
|
||||
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency))
|
||||
fetchExpressAssetsByNetworkIds(userWallet, listOf(currency))
|
||||
currency
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
|
||||
return channelFlow {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
getMultiCurrencyWalletCurrencies(userWallet)
|
||||
|
|
@ -286,7 +303,7 @@ internal class DefaultCurrenciesRepository(
|
|||
userWalletId: UserWalletId,
|
||||
refresh: Boolean,
|
||||
): List<CryptoCurrency> = withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
fetchTokensIfCacheExpired(userWallet, refresh)
|
||||
|
|
@ -298,12 +315,12 @@ internal class DefaultCurrenciesRepository(
|
|||
},
|
||||
)
|
||||
|
||||
responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse)
|
||||
responseCryptoCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse)
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) =
|
||||
withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
val storedTokens = requireNotNull(
|
||||
|
|
@ -313,7 +330,7 @@ internal class DefaultCurrenciesRepository(
|
|||
},
|
||||
)
|
||||
|
||||
responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse)
|
||||
responseCryptoCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse)
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrency(
|
||||
|
|
@ -325,7 +342,7 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency =
|
||||
withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
val response = requireNotNull(
|
||||
|
|
@ -335,7 +352,7 @@ internal class DefaultCurrenciesRepository(
|
|||
},
|
||||
)
|
||||
|
||||
responseCurrenciesFactory.createCurrency(
|
||||
responseCryptoCurrenciesFactory.createCurrency(
|
||||
currencyId = id,
|
||||
response = response,
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse,
|
||||
|
|
@ -348,7 +365,7 @@ internal class DefaultCurrenciesRepository(
|
|||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency.Coin {
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
|
||||
|
|
@ -370,7 +387,10 @@ internal class DefaultCurrenciesRepository(
|
|||
it.derivationPath == derivationPath.value
|
||||
} ?: error("Coin in this network $networkId not found")
|
||||
|
||||
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.requireColdWallet().scanResponse)
|
||||
val coin = responseCryptoCurrenciesFactory.createCurrency(
|
||||
responseToken = storedCoin,
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse,
|
||||
)
|
||||
|
||||
coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
|
||||
}
|
||||
|
|
@ -378,7 +398,7 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
|
||||
return channelFlow {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
|
||||
if (userWallet.isMultiCurrency) {
|
||||
getSavedUserTokensResponse(userWalletId)
|
||||
|
|
@ -394,7 +414,7 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean> {
|
||||
return channelFlow {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
|
||||
if (userWallet.isMultiCurrency) {
|
||||
getSavedUserTokensResponse(userWalletId)
|
||||
|
|
@ -466,7 +486,7 @@ internal class DefaultCurrenciesRepository(
|
|||
contractAddress: String,
|
||||
networkId: String,
|
||||
): CryptoCurrency.Token {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
val token = withContext(dispatchers.io) {
|
||||
val foundToken = tangemTechApi.getCoins(
|
||||
contractAddress = contractAddress,
|
||||
|
|
@ -525,7 +545,7 @@ internal class DefaultCurrenciesRepository(
|
|||
getL2CompatibilityTokenComparison(it, currencyRawId.value)
|
||||
}
|
||||
|
||||
responseCurrenciesFactory.createCurrencies(
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = storedTokens.copy(tokens = filterResponse),
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse,
|
||||
)
|
||||
|
|
@ -570,9 +590,13 @@ internal class DefaultCurrenciesRepository(
|
|||
)
|
||||
}
|
||||
|
||||
override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver {
|
||||
return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
|
||||
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {
|
||||
return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
|
||||
responseCurrenciesFactory.createCurrencies(
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = storedTokens,
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse,
|
||||
)
|
||||
|
|
@ -611,7 +635,7 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
userTokensSaver.store(userWalletId, compatibleUserTokensResponse)
|
||||
|
||||
fetchExpressAssetsByNetworkIds(userWalletId, compatibleUserTokensResponse)
|
||||
fetchExpressAssetsByNetworkIds(userWallet, compatibleUserTokensResponse)
|
||||
}
|
||||
|
||||
private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet.Cold): Boolean {
|
||||
|
|
@ -620,7 +644,7 @@ internal class DefaultCurrenciesRepository(
|
|||
return demoConfig.isDemoCardId(userWallet.cardId) && response == null
|
||||
}
|
||||
|
||||
private suspend fun fetchExpressAssetsByNetworkIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
|
||||
private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) {
|
||||
val tokens = userTokens.tokens.map { token ->
|
||||
LeastTokenInfo(
|
||||
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
|
||||
|
|
@ -629,12 +653,12 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
|
||||
coroutineScope {
|
||||
launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) }
|
||||
launch { expressServiceLoader.update(userWallet, tokens) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchExpressAssetsByNetworkIds(
|
||||
userWalletId: UserWalletId,
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
refresh: Boolean = false,
|
||||
) {
|
||||
|
|
@ -646,11 +670,11 @@ internal class DefaultCurrenciesRepository(
|
|||
)
|
||||
}
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getAssetsCacheKey(userWalletId),
|
||||
key = getAssetsCacheKey(userWallet.walletId),
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
coroutineScope {
|
||||
launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) }
|
||||
launch { expressServiceLoader.update(userWallet, tokens) }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -660,9 +684,8 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {
|
||||
val userWalletId = userWallet.walletId
|
||||
val response = appPreferencesStore.getObjectSyncOrNull(
|
||||
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
|
||||
) ?: createDefaultUserTokensResponse(userWallet)
|
||||
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
|
||||
?: createDefaultUserTokensResponse(userWallet = userWallet)
|
||||
|
||||
if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
|
||||
Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId")
|
||||
|
|
@ -678,20 +701,14 @@ internal class DefaultCurrenciesRepository(
|
|||
private fun createDefaultUserTokensResponse(userWallet: UserWallet) =
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(
|
||||
userWallet.requireColdWallet().scanResponse,
|
||||
userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
|
||||
),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
|
||||
private fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find a user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
|
||||
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected)
|
||||
}
|
||||
|
|
@ -720,14 +737,10 @@ internal class DefaultCurrenciesRepository(
|
|||
private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}"
|
||||
|
||||
private fun getSavedUserTokensResponse(key: UserWalletId): Flow<UserTokensResponse> {
|
||||
return appPreferencesStore
|
||||
.getObject<UserTokensResponse>(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue))
|
||||
.filterNotNull()
|
||||
return userTokensResponseStore.get(userWalletId = key).filterNotNull()
|
||||
}
|
||||
|
||||
private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? {
|
||||
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
|
||||
key = PreferencesKeys.getUserTokensKey(key.stringValue),
|
||||
)
|
||||
return userTokensResponseStore.getSyncOrNull(userWalletId = key)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import arrow.atomic.AtomicBoolean
|
|||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
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.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -15,11 +16,15 @@ import timber.log.Timber
|
|||
/**
|
||||
* Responsible for merging custom tokens into a user's token response.
|
||||
* It handles the logic to update tokens with additional details if necessary.
|
||||
*
|
||||
* @property tangemTechApi Tangem Tech API
|
||||
* @property userTokensSaver user tokens saver
|
||||
* @property dispatchers dispatchers
|
||||
*/
|
||||
internal class CustomTokensMerger(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -29,21 +34,17 @@ internal class CustomTokensMerger(
|
|||
* is needed, and if so, updating the token from [TangemTechApi.getCoins] response. It then pushes to the backend
|
||||
* and returns an updated UserTokensResponse.
|
||||
*
|
||||
* @param userWalletId The identifier for the user's wallet, used when pushing updates.
|
||||
* @param response The original user tokens response that may need to be updated.
|
||||
* @return A potentially updated UserTokensResponse, with custom tokens merged if necessary.
|
||||
* @param userWalletId the identifier for the user's wallet, used when pushing updates
|
||||
* @param response the original user tokens response that may need to be updated
|
||||
*
|
||||
* @return a potentially updated UserTokensResponse, with custom tokens merged if necessary
|
||||
*/
|
||||
suspend fun mergeIfPresented(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse {
|
||||
// use flag to check: we can't compare two token list after merge because Token equals don't include some fields
|
||||
val wasMerged = AtomicBoolean(false)
|
||||
|
||||
val mergedTokens = withContext(dispatchers.default) {
|
||||
response.tokens
|
||||
.map { token ->
|
||||
async { mergeIfPresented(token, wasMerged) }
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
val mergedTokens = mergeTokens(response = response, wasMerged = wasMerged)
|
||||
|
||||
val updatedResponse = response.copy(tokens = mergedTokens)
|
||||
|
||||
// previously here was used compare response.tokens, but it's not working correctly
|
||||
|
|
@ -55,33 +56,48 @@ internal class CustomTokensMerger(
|
|||
return updatedResponse
|
||||
}
|
||||
|
||||
private suspend fun mergeIfPresented(
|
||||
token: UserTokensResponse.Token,
|
||||
private suspend fun mergeTokens(
|
||||
response: UserTokensResponse,
|
||||
wasMerged: AtomicBoolean,
|
||||
): UserTokensResponse.Token {
|
||||
if (isCoinOrNonCustomToken(token)) return token
|
||||
): List<UserTokensResponse.Token> {
|
||||
return withContext(dispatchers.default) {
|
||||
response.tokens
|
||||
.map { token ->
|
||||
async {
|
||||
if (isCoinOrNonCustomToken(token)) return@async token
|
||||
|
||||
return merge(token, wasMerged)
|
||||
}
|
||||
|
||||
private suspend fun merge(
|
||||
customToken: UserTokensResponse.Token,
|
||||
wasMerged: AtomicBoolean,
|
||||
): UserTokensResponse.Token {
|
||||
val foundToken = fetchToken(customToken)
|
||||
if (foundToken != null) {
|
||||
wasMerged.set(true)
|
||||
mergeToken(customToken = token, wasMerged = wasMerged)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
return foundToken ?: customToken
|
||||
}
|
||||
|
||||
private fun isCoinOrNonCustomToken(token: UserTokensResponse.Token): Boolean {
|
||||
return token.contractAddress.isNullOrEmpty() || token.id != null
|
||||
}
|
||||
|
||||
private suspend fun fetchToken(token: UserTokensResponse.Token): UserTokensResponse.Token? {
|
||||
val response = withContext(dispatchers.io) {
|
||||
safeApiCall(
|
||||
private suspend fun mergeToken(
|
||||
customToken: UserTokensResponse.Token,
|
||||
wasMerged: AtomicBoolean,
|
||||
): UserTokensResponse.Token {
|
||||
val foundToken = findToken(token = customToken)
|
||||
|
||||
return if (foundToken != null) {
|
||||
wasMerged.set(true)
|
||||
customToken.mergeWith(foundToken)
|
||||
} else {
|
||||
customToken
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a [token] among the cryptocurrencies available for application.
|
||||
* If result is not null, then the token is available and should not be custom.
|
||||
*/
|
||||
private suspend fun findToken(token: UserTokensResponse.Token): CoinsResponse.Coin? {
|
||||
return withContext(dispatchers.io) {
|
||||
val response = safeApiCall(
|
||||
call = {
|
||||
tangemTechApi.getCoins(
|
||||
contractAddress = token.contractAddress,
|
||||
|
|
@ -89,19 +105,20 @@ internal class CustomTokensMerger(
|
|||
).bind()
|
||||
},
|
||||
onError = {
|
||||
Timber.w(it, "Unable to fetch token")
|
||||
Timber.e(it, "Unable to fetch token:\n$token")
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
val foundToken = response?.coins?.firstOrNull() ?: return null
|
||||
|
||||
return token.copy(
|
||||
id = foundToken.id,
|
||||
name = foundToken.name,
|
||||
symbol = foundToken.symbol.ifEmpty {
|
||||
token.symbol
|
||||
},
|
||||
response?.coins?.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.Token.mergeWith(coin: CoinsResponse.Coin): UserTokensResponse.Token {
|
||||
return copy(
|
||||
id = coin.id,
|
||||
name = coin.name,
|
||||
symbol = coin.symbol.ifEmpty { symbol },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.tokens.model.FoundToken
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object FoundTokenConverter : Converter<CoinsResponse.Coin, FoundToken> {
|
||||
|
||||
override fun convert(value: CoinsResponse.Coin): FoundToken {
|
||||
return FoundToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
contractAddress = requireNotNull(value.networks.first().contractAddress),
|
||||
decimals = requireNotNull(value.networks.first().decimalCount).intValueExact(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
|
||||
/**
|
||||
* Adapter to replace unsupported currencies for quotes request if it necessary
|
||||
*/
|
||||
class QuotesUnsupportedCurrenciesIdAdapter {
|
||||
|
||||
/**
|
||||
* Replaces unsupported currencies id to it replacements for request
|
||||
*/
|
||||
fun replaceUnsupportedCurrencies(currenciesIds: Set<String>): ReplacementResult {
|
||||
val idsForRequest = mutableSetOf<String>()
|
||||
val idsFiltered = mutableSetOf<String>()
|
||||
currenciesIds.forEach { currencyId ->
|
||||
val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[currencyId]
|
||||
if (replacementId != null) {
|
||||
idsFiltered.add(currencyId)
|
||||
idsForRequest.add(replacementId)
|
||||
} else {
|
||||
idsForRequest.add(currencyId)
|
||||
}
|
||||
}
|
||||
return ReplacementResult(idsForRequest = idsForRequest, idsFiltered = idsFiltered)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover previously replaced currencies to response that uses in application
|
||||
*/
|
||||
fun getResponseWithUnsupportedCurrencies(response: QuotesResponse, filteredIds: Set<String>): QuotesResponse {
|
||||
val updatedQuotes = mutableMapOf<String, QuotesResponse.Quote>()
|
||||
filteredIds.forEach { filteredId ->
|
||||
val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[filteredId]
|
||||
if (replacementId != null) {
|
||||
val quoteReplacement = response.quotes[replacementId]
|
||||
if (quoteReplacement != null) {
|
||||
updatedQuotes[filteredId] = quoteReplacement
|
||||
}
|
||||
}
|
||||
}
|
||||
return response.copy(quotes = updatedQuotes + response.quotes)
|
||||
}
|
||||
|
||||
data class ReplacementResult(val idsForRequest: Set<String>, val idsFiltered: Set<String>)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Map that contains unsupported currencies and their replacement for request
|
||||
*/
|
||||
private val ethCoinId = Blockchain.Ethereum.toCoinId()
|
||||
private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = l2BlockchainsCoinIds.associateWith { ethCoinId }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,481 @@
|
|||
package com.tangem.data.tokens
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.utils.assertEither
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
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.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.DemoConfig
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
|
||||
|
||||
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
|
||||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
|
||||
private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true)
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
private val customTokensMerger: CustomTokensMerger = mockk()
|
||||
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 fetcher = DefaultMultiWalletCryptoCurrenciesFetcher(
|
||||
demoConfig = DemoConfig(),
|
||||
userWalletsStore = userWalletsStore,
|
||||
tangemTechApi = tangemTechApi,
|
||||
customTokensMerger = customTokensMerger,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(
|
||||
userWalletsStore,
|
||||
tangemTechApi,
|
||||
userTokensResponseStore,
|
||||
userTokensSaver,
|
||||
cardCryptoCurrencyFactory,
|
||||
expressServiceLoader,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch failure if UserWallet ISN'T MULTI-CURRENCY wallet`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
|
||||
val mockUserWallet = mockk<UserWallet> {
|
||||
every { isMultiCurrency } returns false
|
||||
}
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException("${this::class.simpleName} supports only multi-currency wallet").left()
|
||||
assertEither(actual, expected)
|
||||
|
||||
verifyOrder { userWalletsStore.getSyncStrict(key = params.userWalletId) }
|
||||
coVerify(inverse = true) {
|
||||
userTokensResponseStore.getSyncOrNull(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if CARD IS DEMO and STORED TOKENS ARE EMPTY`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
|
||||
val mockUserWallet = mockk<UserWallet.Cold> {
|
||||
every { walletId } returns userWalletId
|
||||
every { isMultiCurrency } returns true
|
||||
every { cardId } returns "AC01000000041225"
|
||||
}
|
||||
|
||||
val defaultCoins = listOf(
|
||||
cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin),
|
||||
cryptoCurrencyFactory.createCoin(Blockchain.Ethereum),
|
||||
)
|
||||
|
||||
val userTokensResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
tokens = listOf(
|
||||
userTokensResponseFactory.createResponseToken(defaultCoins.first()),
|
||||
userTokensResponseFactory.createResponseToken(defaultCoins.last()),
|
||||
),
|
||||
)
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null
|
||||
every {
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse)
|
||||
} returns defaultCoins
|
||||
coEvery {
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
} returns userTokensResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId)
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = mockUserWallet.scanResponse)
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if CARD IS DEMO and STORED TOKENS AREN'T EMPTY`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
|
||||
val mockUserWallet = mockk<UserWallet.Cold> {
|
||||
every { walletId } returns userWalletId
|
||||
every { isMultiCurrency } returns true
|
||||
every { cardId } returns "AC01000000041225"
|
||||
}
|
||||
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN),
|
||||
)
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns defaultResponse
|
||||
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
|
||||
coEvery {
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
|
||||
} returns apiResponse.data
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId)
|
||||
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())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if CARD ISN'T DEMO`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
|
||||
val mockUserWallet = mockk<UserWallet.Cold> {
|
||||
every { walletId } returns userWalletId
|
||||
every { isMultiCurrency } returns true
|
||||
every { cardId } returns "cardID"
|
||||
}
|
||||
|
||||
val apiResponse = ApiResponse.Success(
|
||||
data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN),
|
||||
)
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
|
||||
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
|
||||
coEvery {
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
|
||||
} returns apiResponse.data
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
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())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId = any())
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
|
||||
val mockUserWallet = mockk<UserWallet.Cold> {
|
||||
every { walletId } returns userWalletId
|
||||
every { isMultiCurrency } returns true
|
||||
every { cardId } returns "cardID"
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val apiResponse = ApiResponse.Error(
|
||||
cause = ApiResponseError.TimeoutException,
|
||||
) as ApiResponse<UserTokensResponse>
|
||||
|
||||
val defaultCoins = listOf(
|
||||
cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin),
|
||||
cryptoCurrencyFactory.createCoin(Blockchain.Ethereum),
|
||||
)
|
||||
|
||||
val userTokensResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
tokens = listOf(
|
||||
userTokensResponseFactory.createResponseToken(defaultCoins.first()),
|
||||
userTokensResponseFactory.createResponseToken(defaultCoins.last()),
|
||||
),
|
||||
)
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
|
||||
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null
|
||||
coEvery {
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse)
|
||||
} returns defaultCoins
|
||||
coEvery {
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
} returns userTokensResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
|
||||
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())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
userTokensSaver.push(userWalletId = any(), response = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
|
||||
val mockUserWallet = mockk<UserWallet.Cold> {
|
||||
every { walletId } returns userWalletId
|
||||
every { isMultiCurrency } returns true
|
||||
every { cardId } returns "cardID"
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val apiResponse = ApiResponse.Error(
|
||||
cause = ApiResponseError.TimeoutException,
|
||||
) as ApiResponse<UserTokensResponse>
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
|
||||
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse
|
||||
coEvery {
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
|
||||
} returns defaultResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
|
||||
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())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
userTokensSaver.push(userWalletId = any(), response = any())
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
|
||||
val mockUserWallet = mockk<UserWallet.Cold> {
|
||||
every { walletId } returns userWalletId
|
||||
every { isMultiCurrency } returns true
|
||||
every { cardId } returns "cardID"
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val apiResponse = ApiResponse.Error(
|
||||
cause = ApiResponseError.HttpException(
|
||||
code = ApiResponseError.HttpException.Code.NOT_FOUND,
|
||||
message = null,
|
||||
errorBody = null,
|
||||
),
|
||||
) as ApiResponse<UserTokensResponse>
|
||||
|
||||
val defaultCoins = listOf(
|
||||
cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin),
|
||||
cryptoCurrencyFactory.createCoin(Blockchain.Ethereum),
|
||||
)
|
||||
|
||||
val userTokensResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
tokens = listOf(
|
||||
userTokensResponseFactory.createResponseToken(defaultCoins.first()),
|
||||
userTokensResponseFactory.createResponseToken(defaultCoins.last()),
|
||||
),
|
||||
)
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
|
||||
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null
|
||||
coEvery {
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse)
|
||||
} returns defaultCoins
|
||||
coEvery {
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
|
||||
} returns userTokensResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse)
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest {
|
||||
// Arrange
|
||||
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
|
||||
|
||||
val mockUserWallet = mockk<UserWallet.Cold> {
|
||||
every { walletId } returns userWalletId
|
||||
every { isMultiCurrency } returns true
|
||||
every { cardId } returns "cardID"
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val apiResponse = ApiResponse.Error(
|
||||
cause = ApiResponseError.HttpException(
|
||||
code = ApiResponseError.HttpException.Code.NOT_FOUND,
|
||||
message = null,
|
||||
errorBody = null,
|
||||
),
|
||||
) as ApiResponse<UserTokensResponse>
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
|
||||
coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse
|
||||
coEvery {
|
||||
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
|
||||
} returns defaultResponse
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
assertEither(actual, expected)
|
||||
|
||||
coVerifyOrder {
|
||||
userWalletsStore.getSyncStrict(key = params.userWalletId)
|
||||
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
|
||||
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())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any())
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("011")
|
||||
|
||||
val defaultResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
tokens = listOf(
|
||||
UserTokensResponse.Token(
|
||||
id = null,
|
||||
networkId = "bitcoin",
|
||||
derivationPath = null,
|
||||
name = "Bitcoin",
|
||||
symbol = "BTC",
|
||||
decimals = 8,
|
||||
contractAddress = null,
|
||||
addresses = listOf(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
fun UserTokensResponse.toLeastTokens(): List<LeastTokenInfo> {
|
||||
return tokens.map { token ->
|
||||
LeastTokenInfo(
|
||||
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
|
||||
network = token.networkId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
package com.tangem.data.tokens
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
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.getEmittedValues
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
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.common.configs.GenericCardConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultMultiWalletCryptoCurrenciesProducerTest {
|
||||
|
||||
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
|
||||
|
||||
private val params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWallet.walletId)
|
||||
private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true)
|
||||
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
|
||||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory = mockk()
|
||||
|
||||
private val producer = DefaultMultiWalletCryptoCurrenciesProducer(
|
||||
params = params,
|
||||
userWalletsStore = userWalletsStore,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(userWalletsStore, userTokensResponseStore, responseCryptoCurrenciesFactory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is mapped for user wallet id from params`() = runTest {
|
||||
// Arrange
|
||||
val userTokensResponseFlow = flowOf<UserTokensResponse?>(null)
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet
|
||||
every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = emptySet<CryptoCurrency>()
|
||||
|
||||
Truth.assertThat(actual.size).isEqualTo(1)
|
||||
Truth.assertThat(actual.first()).isEqualTo(expected)
|
||||
|
||||
verifyOrder {
|
||||
userWalletsStore.getSyncStrict(params.userWalletId)
|
||||
userTokensResponseStore.get(params.userWalletId)
|
||||
}
|
||||
|
||||
verify(inverse = true) {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow will updated if UserTokensResponse is updated`() = runTest {
|
||||
// Arrange
|
||||
val userTokensResponseFlow = MutableSharedFlow<UserTokensResponse>(replay = 2)
|
||||
|
||||
val userTokensResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.TOKEN,
|
||||
sort = UserTokensResponse.SortType.MARKETCAP,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
val cryptoCurrencies = emptySet<CryptoCurrency>()
|
||||
|
||||
val updatedUserTokensResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.TOKEN,
|
||||
sort = UserTokensResponse.SortType.MARKETCAP,
|
||||
tokens = listOf(
|
||||
UserTokensResponse.Token(
|
||||
id = null,
|
||||
networkId = "bitcoin",
|
||||
derivationPath = null,
|
||||
name = "Bitcoin",
|
||||
symbol = "BTC",
|
||||
decimals = 8,
|
||||
contractAddress = null,
|
||||
addresses = listOf(),
|
||||
),
|
||||
),
|
||||
)
|
||||
val updatedCryptoCurrencies = setOf(
|
||||
cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin),
|
||||
)
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet
|
||||
every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow
|
||||
|
||||
every {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
} returns cryptoCurrencies.toList()
|
||||
|
||||
every {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = updatedUserTokensResponse,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
} returns updatedCryptoCurrencies.toList()
|
||||
|
||||
val producerFlow = producer.produce()
|
||||
|
||||
// Act 1 (first emit)
|
||||
userTokensResponseFlow.emit(userTokensResponse)
|
||||
|
||||
val actual1 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// Assert
|
||||
val expected1 = cryptoCurrencies
|
||||
|
||||
Truth.assertThat(actual1.size).isEqualTo(1)
|
||||
Truth.assertThat(actual1.first()).isEqualTo(expected1)
|
||||
|
||||
verifyOrder {
|
||||
userWalletsStore.getSyncStrict(params.userWalletId)
|
||||
userTokensResponseStore.get(params.userWalletId)
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
}
|
||||
|
||||
// Act 2 (second emit)
|
||||
userTokensResponseFlow.emit(updatedUserTokensResponse)
|
||||
|
||||
val actual2 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// Assert
|
||||
val expected2 = listOf(cryptoCurrencies, updatedCryptoCurrencies)
|
||||
|
||||
Truth.assertThat(actual2.size).isEqualTo(2)
|
||||
Truth.assertThat(actual2).isEqualTo(expected2)
|
||||
|
||||
verifyOrder {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = updatedUserTokensResponse,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is filtered the same status`() = runTest {
|
||||
// Arrange
|
||||
val userTokensResponseFlow = MutableSharedFlow<UserTokensResponse>(replay = 2)
|
||||
|
||||
val userTokensResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.TOKEN,
|
||||
sort = UserTokensResponse.SortType.MARKETCAP,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val cryptoCurrencies = emptySet<CryptoCurrency>()
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet
|
||||
every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow
|
||||
|
||||
every {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
} returns cryptoCurrencies.toList()
|
||||
|
||||
val producerFlow = producer.produce()
|
||||
|
||||
// Act 1 (first emit)
|
||||
userTokensResponseFlow.emit(userTokensResponse)
|
||||
|
||||
val actual1 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// Assert
|
||||
val expected1 = cryptoCurrencies
|
||||
|
||||
Truth.assertThat(actual1.size).isEqualTo(1)
|
||||
Truth.assertThat(actual1.first()).isEqualTo(expected1)
|
||||
|
||||
verifyOrder {
|
||||
userWalletsStore.getSyncStrict(params.userWalletId)
|
||||
userTokensResponseStore.get(params.userWalletId)
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
}
|
||||
|
||||
// Act 2 (second emit)
|
||||
userTokensResponseFlow.emit(userTokensResponse)
|
||||
|
||||
val actual2 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// Assert
|
||||
val expected2 = expected1
|
||||
Truth.assertThat(actual2.size).isEqualTo(1)
|
||||
Truth.assertThat(actual2.first()).isEqualTo(expected2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = IllegalStateException()
|
||||
|
||||
val userTokensResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.TOKEN,
|
||||
sort = UserTokensResponse.SortType.MARKETCAP,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
val cryptoCurrencies = emptySet<CryptoCurrency>()
|
||||
|
||||
val innerFlow = MutableStateFlow(value = false)
|
||||
val userTokensResponseFlow = flow {
|
||||
if (innerFlow.value) {
|
||||
emit(userTokensResponse)
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
.buffer(capacity = 5)
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet
|
||||
every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow
|
||||
|
||||
every {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
} returns cryptoCurrencies.toList()
|
||||
|
||||
val producerFlow = producer.produceWithFallback()
|
||||
|
||||
// Act 1 (fallback)
|
||||
val actual1 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// Assert
|
||||
val expected1 = producer.fallback
|
||||
Truth.assertThat(actual1.size).isEqualTo(1)
|
||||
Truth.assertThat(actual1.first()).isEqualTo(expected1)
|
||||
|
||||
verifyOrder {
|
||||
userWalletsStore.getSyncStrict(params.userWalletId)
|
||||
userTokensResponseStore.get(params.userWalletId)
|
||||
}
|
||||
|
||||
// Act 2 (emit)
|
||||
innerFlow.emit(value = true)
|
||||
val actual2 = getEmittedValues(flow = producerFlow)
|
||||
|
||||
// Assert
|
||||
val expected2 = cryptoCurrencies
|
||||
Truth.assertThat(actual2.size).isEqualTo(1)
|
||||
Truth.assertThat(actual2.first()).isEqualTo(expected2)
|
||||
|
||||
verifyOrder {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
response = userTokensResponse,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow is empty if store returns empty flow`() = runTest {
|
||||
// Arrange
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet
|
||||
every { userTokensResponseStore.get(params.userWalletId) } returns emptyFlow()
|
||||
|
||||
// Act
|
||||
val actual = producer.produce().let(::getEmittedValues)
|
||||
|
||||
// Assert
|
||||
val expected = producer.fallback
|
||||
Truth.assertThat(actual.size).isEqualTo(1)
|
||||
Truth.assertThat(actual.first()).isEqualTo(expected)
|
||||
|
||||
verifyOrder {
|
||||
userWalletsStore.getSyncStrict(params.userWalletId)
|
||||
userTokensResponseStore.get(params.userWalletId)
|
||||
}
|
||||
|
||||
verify(inverse = true) {
|
||||
responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `produce throws exception if UserWallet isn't multi-currency wallet`() = runTest {
|
||||
// Arrange
|
||||
val mockUserWallet = mockk<UserWallet> {
|
||||
every { isMultiCurrency } returns false
|
||||
}
|
||||
|
||||
every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet
|
||||
|
||||
// Act
|
||||
val actual = runCatching { producer.produce() }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
val expected = IllegalStateException(
|
||||
"${DefaultMultiWalletCryptoCurrenciesProducer::class.simpleName} supports only multi-currency wallet",
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message)
|
||||
|
||||
verifyOrder { userWalletsStore.getSyncStrict(params.userWalletId) }
|
||||
|
||||
verify(inverse = true) {
|
||||
userTokensResponseStore.get(any())
|
||||
responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any())
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val scanResponse = MockScanResponseFactory.create(
|
||||
cardConfig = GenericCardConfig(2),
|
||||
derivedKeys = emptyMap(),
|
||||
)
|
||||
|
||||
val userWallet = MockUserWalletFactory.create(scanResponse = scanResponse)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
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.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class CustomTokensMergerTest {
|
||||
|
||||
private val tangemTechApi: TangemTechApi = mockk()
|
||||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val merger = CustomTokensMerger(
|
||||
tangemTechApi = tangemTechApi,
|
||||
userTokensSaver = userTokensSaver,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(tangemTechApi, userTokensSaver)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mergeIfPresented successfully if UserTokensResponse DOESN'T CONTAIN custom tokens`() = runTest {
|
||||
// Act
|
||||
val actual = merger.mergeIfPresented(userWalletId = userWalletId, response = defaultUserTokensResponse)
|
||||
|
||||
// Assert
|
||||
val expected = defaultUserTokensResponse
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(inverse = true) {
|
||||
tangemTechApi.getCoins(contractAddress = any(), networkIds = any())
|
||||
userTokensSaver.push(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mergeIfPresented successfully if UserTokensResponse CONTAINS custom tokens`() = runTest {
|
||||
// Arrange
|
||||
val customToken = UserTokensResponse.Token(
|
||||
id = null,
|
||||
networkId = "cum",
|
||||
name = "Matilda Rich",
|
||||
symbol = "consectetuer",
|
||||
decimals = 9582,
|
||||
contractAddress = "contractAddress",
|
||||
)
|
||||
|
||||
val userTokensResponse = defaultUserTokensResponse.copy(
|
||||
tokens = defaultUserTokensResponse.tokens + customToken,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getCoins(contractAddress = customToken.contractAddress, networkIds = customToken.networkId)
|
||||
} returns ApiResponse.Success(data = createCoinResponse(token = customToken))
|
||||
|
||||
// Act
|
||||
val actual = merger.mergeIfPresented(userWalletId = userWalletId, response = userTokensResponse)
|
||||
|
||||
// Assert
|
||||
val updatedCustomToken = customToken.copy(
|
||||
id = "NEW_${customToken.id}",
|
||||
name = "NEW_${customToken.name}",
|
||||
symbol = "NEW_${customToken.symbol}",
|
||||
)
|
||||
|
||||
val expected = defaultUserTokensResponse.copy(
|
||||
tokens = defaultUserTokensResponse.tokens + updatedCustomToken,
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
tangemTechApi.getCoins(
|
||||
contractAddress = customToken.contractAddress,
|
||||
networkIds = customToken.networkId,
|
||||
)
|
||||
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mergeIfPresented successfully if API returns error`() = runTest {
|
||||
// Arrange
|
||||
val customToken = UserTokensResponse.Token(
|
||||
id = null,
|
||||
networkId = "cum",
|
||||
name = "Matilda Rich",
|
||||
symbol = "consectetuer",
|
||||
decimals = 9582,
|
||||
contractAddress = "contractAddress",
|
||||
)
|
||||
|
||||
val userTokensResponse = defaultUserTokensResponse.copy(
|
||||
tokens = defaultUserTokensResponse.tokens + customToken,
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException) as ApiResponse<CoinsResponse>
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.getCoins(contractAddress = customToken.contractAddress, networkIds = customToken.networkId)
|
||||
} returns apiResponse
|
||||
|
||||
// Act
|
||||
val actual = merger.mergeIfPresented(userWalletId = userWalletId, response = userTokensResponse)
|
||||
|
||||
// Assert
|
||||
val expected = userTokensResponse
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify {
|
||||
tangemTechApi.getCoins(
|
||||
contractAddress = customToken.contractAddress,
|
||||
networkIds = customToken.networkId,
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { userTokensSaver.push(userWalletId = any(), response = any()) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
|
||||
val defaultUserTokensResponse = UserTokensResponseFactory().createUserTokensResponse(
|
||||
currencies = MockCryptoCurrencyFactory().ethereumAndStellar,
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
|
||||
fun createCoinResponse(token: UserTokensResponse.Token): CoinsResponse {
|
||||
return CoinsResponse(
|
||||
imageHost = null,
|
||||
coins = listOf(
|
||||
CoinsResponse.Coin(
|
||||
id = "NEW_${token.id}",
|
||||
name = "NEW_${token.name}",
|
||||
symbol = "NEW_${token.symbol}",
|
||||
active = true,
|
||||
),
|
||||
),
|
||||
total = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue