Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-01 13:36:38 +04:00
parent b89a6690eb
commit ad9de30a01
5 changed files with 678 additions and 1 deletions

View file

@ -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,
)
}
}

View file

@ -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,
)
}
}

View file

@ -575,7 +575,7 @@ internal class DefaultCurrenciesRepository(
}
override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver {
return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver
return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY]
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {

View file

@ -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,
)
}
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.tokens
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.wallets.models.UserWalletId
/**
* Fetcher of crypto currencies for a multi-currency wallet with [UserWalletId]
*
[REDACTED_AUTHOR]
*/
interface MultiWalletCryptoCurrenciesFetcher : FlowFetcher<MultiWalletCryptoCurrenciesFetcher.Params> {
data class Params(val userWalletId: UserWalletId)
}