Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-31 11:09:21 +04:00
parent ded9de148c
commit 377f2cd6fd
8 changed files with 406 additions and 1 deletions

View file

@ -31,6 +31,14 @@ class ResponseCryptoCurrenciesFactory(
.toList() .toList()
} }
fun createCurrencies(tokens: List<UserTokensResponse.Token>, scanResponse: ScanResponse): List<CryptoCurrency> {
return tokens
.asSequence()
.mapNotNull { createCurrency(it, scanResponse) }
.distinctBy(CryptoCurrency::id)
.toList()
}
fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? { fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? {
var blockchain = Blockchain.fromNetworkId(responseToken.networkId) var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
if (blockchain == null || blockchain == Blockchain.Unknown) { if (blockchain == null || blockchain == Blockchain.Unknown) {

View file

@ -0,0 +1,46 @@
package com.tangem.data.networks.multi
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import javax.inject.Inject
/**
* Default implementation of [MultiNetworkStatusFetcher]
*
* @property singleNetworkStatusFetcher single network status fetcher
*
[REDACTED_AUTHOR]
*/
internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
) : MultiNetworkStatusFetcher {
override suspend fun invoke(params: MultiNetworkStatusFetcher.Params): Either<Throwable, Unit> = either {
val result = coroutineScope {
params.networks
.map {
async {
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params(
userWalletId = params.userWalletId,
network = it,
),
)
}
}
.awaitAll()
}
val failedResult = result.firstOrNull { it.isLeft() }
ensure(failedResult == null) {
IllegalStateException("Failed to fetch network statuses")
}
}
}

View file

@ -0,0 +1,114 @@
package com.tangem.data.networks.single
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
/**
* Default implementation of [SingleNetworkStatusFetcher]
*
* @param excludedBlockchains excluded blockchains
* @property walletManagersFacade wallet managers facade
* @property networksStatusesStore networks statuses store
* @property userWalletsStore user wallets store
* @property appPreferencesStore app preferences store
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
excludedBlockchains: ExcludedBlockchains,
private val walletManagersFacade: WalletManagersFacade,
private val networksStatusesStore: NetworksStatusesStoreV2,
private val userWalletsStore: UserWalletsStore,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : SingleNetworkStatusFetcher {
private val demoConfig = DemoConfig()
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains)
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
private val networkStatusFactory = NetworkStatusFactory()
override suspend fun invoke(params: SingleNetworkStatusFetcher.Params): Either<Throwable, Unit> = Either.catch {
networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network)
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
val networkCurrencies = createCurrencies(userWallet = userWallet, network = params.network)
val result = withContext(dispatchers.io) {
walletManagersFacade.update(
userWalletId = params.userWalletId,
network = params.network,
extraTokens = networkCurrencies
.filterIsInstance<CryptoCurrency.Token>()
.toSet(),
)
}
val status = networkStatusFactory.createNetworkStatus(
network = params.network,
result = result,
currencies = networkCurrencies.toSet(),
)
networksStatusesStore.storeActual(userWalletId = params.userWalletId, value = status)
}
.onLeft {
Timber.e("Failed to fetch network status for $params: $it")
networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network)
}
private suspend fun createCurrencies(userWallet: UserWallet, network: Network): List<CryptoCurrency> {
val blockchain = Blockchain.fromNetworkId(networkId = network.backendId)
// multi-currency wallet
if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network)
// check if the blockchain of single-currency wallet is the same as network
val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
if (cardBlockchain != blockchain) return emptyList()
// single-currency wallet with token (NODL)
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
return cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
}
// single-currency wallet
return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf)
}
private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List<CryptoCurrency> {
val response = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue),
) ?: return emptyList()
return responseCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter {
it.networkId == network.backendId && it.derivationPath == network.derivationPath.value
},
scanResponse = userWallet.scanResponse,
)
}
}

View file

@ -0,0 +1,119 @@
package com.tangem.data.networks.multi
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.wallets.models.UserWalletId
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class DefaultMultiNetworkStatusFetcherTest {
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk()
private val fetcher = DefaultMultiNetworkStatusFetcher(singleNetworkStatusFetcher = singleNetworkStatusFetcher)
@Test
fun `fetch networks statuses successfully`() = runTest {
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar)
val ethParams = SingleNetworkStatusFetcher.Params(
userWalletId = userWalletId,
network = ethereumAndStellar.first(),
)
val stellarParams = SingleNetworkStatusFetcher.Params(
userWalletId = userWalletId,
network = ethereumAndStellar.last(),
)
coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right()
coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right()
val actual = fetcher(params)
coVerify {
singleNetworkStatusFetcher(ethParams)
singleNetworkStatusFetcher(stellarParams)
}
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch networks statuses failure if one of them fails`() = runTest {
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar)
val ethParams = SingleNetworkStatusFetcher.Params(
userWalletId = userWalletId,
network = ethereumAndStellar.first(),
)
val stellarParams = SingleNetworkStatusFetcher.Params(
userWalletId = userWalletId,
network = ethereumAndStellar.last(),
)
val ethException = IllegalStateException("eth")
coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left()
coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right()
val actual = fetcher(params)
coVerify {
singleNetworkStatusFetcher(ethParams)
singleNetworkStatusFetcher(stellarParams)
}
val expected = IllegalStateException("Failed to fetch network statuses")
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
@Test
fun `fetch networks statuses failure if all of them fails`() = runTest {
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar)
val ethParams = SingleNetworkStatusFetcher.Params(
userWalletId = userWalletId,
network = ethereumAndStellar.first(),
)
val stellarParams = SingleNetworkStatusFetcher.Params(
userWalletId = userWalletId,
network = ethereumAndStellar.last(),
)
coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left()
coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left()
val actual = fetcher(params)
coVerify {
singleNetworkStatusFetcher(ethParams)
singleNetworkStatusFetcher(stellarParams)
}
val expected = IllegalStateException("Failed to fetch network statuses")
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
}
private companion object {
val userWalletId = UserWalletId("011")
val ethereumAndStellar = MockCryptoCurrencyFactory().ethereumAndStellar.map { it.network }.toSet()
}
}

View file

@ -0,0 +1,88 @@
package com.tangem.data.networks.single
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.Ordering
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class DefaultSingleNetworkStatusFetcherTest {
private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true)
private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true)
private val userWalletsStore: UserWalletsStore = mockk(relaxed = true)
private val fetcher = DefaultSingleNetworkStatusFetcher(
excludedBlockchains = mockk(relaxed = true),
walletManagersFacade = walletManagersFacade,
networksStatusesStore = networksStatusesStore,
userWalletsStore = userWalletsStore,
appPreferencesStore = mockk(relaxed = true),
dispatchers = TestingCoroutineDispatcherProvider(),
)
@Test
fun `fetch network status successfully`() = runTest {
val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network)
val result = UpdateWalletManagerResult.MissedDerivation
coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result
val actual = fetcher(params)
coVerify(ordering = Ordering.SEQUENCE) {
networksStatusesStore.refresh(userWalletId = userWalletId, network = network)
userWalletsStore.getSyncStrict(key = userWalletId)
walletManagersFacade.update(userWalletId, network, emptySet())
networksStatusesStore.storeActual(
userWalletId = userWalletId,
value = NetworkStatus(network, NetworkStatus.MissedDerivation),
)
}
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch network status failure`() = runTest {
val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network)
val exception = IllegalStateException()
coEvery { userWalletsStore.getSyncStrict(key = userWalletId) } throws exception
val actual = fetcher(params)
coVerify(ordering = Ordering.SEQUENCE) {
networksStatusesStore.refresh(userWalletId = userWalletId, network = network)
userWalletsStore.getSyncStrict(key = userWalletId)
networksStatusesStore.storeError(userWalletId = userWalletId, network = network)
}
coVerify(inverse = true) {
walletManagersFacade.update(userWalletId = any(), network = any(), extraTokens = any())
networksStatusesStore.storeActual(userWalletId = any(), value = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isEqualTo(exception)
}
private companion object {
val userWalletId = UserWalletId("011")
val network = MockCryptoCurrencyFactory().ethereum.network
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import timber.log.Timber import timber.log.Timber
internal class NetworkStatusFactory { class NetworkStatusFactory {
fun createNetworkStatus( fun createNetworkStatus(
network: Network, network: Network,

View file

@ -0,0 +1,15 @@
package com.tangem.domain.networks.multi
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
/**
* Fetcher of network status [Network] for wallet with [UserWalletId]
*
[REDACTED_AUTHOR]
*/
interface MultiNetworkStatusFetcher : FlowFetcher<MultiNetworkStatusFetcher.Params> {
data class Params(val userWalletId: UserWalletId, val networks: Set<Network>)
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.networks.single
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
/**
* Fetcher of network status [Network] for wallet with [UserWalletId]
*
[REDACTED_AUTHOR]
*/
interface SingleNetworkStatusFetcher : FlowFetcher<SingleNetworkStatusFetcher.Params> {
data class Params(val userWalletId: UserWalletId, val network: Network)
}