Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-28 17:00:02 +04:00
parent c38c185a2d
commit 732df0ec4f
11 changed files with 430 additions and 6 deletions

View file

@ -20,11 +20,12 @@ internal class RuntimeUserWalletsStore(
override val userWallets: Flow<List<UserWallet>>
get() = userWalletsListManager.userWallets
override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? {
return userWalletsListManager
.userWallets
.firstOrNull()
?.singleOrNull { it.walletId == key }
override fun getSyncOrNull(key: UserWalletId): UserWallet? {
return userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == key }
}
override suspend fun getSyncStrict(key: UserWalletId): UserWallet {
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
}
override suspend fun getAllSyncOrNull(): List<UserWallet>? {

View file

@ -14,6 +14,8 @@ dependencies {
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.libs.blockchainSdk)

View file

@ -0,0 +1,33 @@
package com.tangem.common.test.domain.wallet
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
/**
[REDACTED_AUTHOR]
*/
object MockUserWalletFactory {
private val defaultScanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(2),
derivedKeys = emptyMap(),
)
fun create(scanResponse: ScanResponse = defaultScanResponse): UserWallet {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()!!
return UserWallet(
walletId = userWalletId,
name = "Wallet 1",
artworkUrl = "",
cardsInWallet = emptySet(),
scanResponse = scanResponse,
isMultiCurrency = scanResponse.cardTypesResolver.isMultiwalletAllowed(),
hasBackupError = false,
)
}
}

View file

@ -11,7 +11,9 @@ interface UserWalletsStore {
val userWallets: Flow<List<UserWallet>>
suspend fun getSyncOrNull(key: UserWalletId): UserWallet?
fun getSyncOrNull(key: UserWalletId): UserWallet?
suspend fun getSyncStrict(key: UserWalletId): UserWallet
suspend fun getAllSyncOrNull(): List<UserWallet>?

View file

@ -22,7 +22,9 @@ dependencies {
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.networks)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(deps.hilt.android)

View file

@ -0,0 +1,66 @@
package com.tangem.data.networks.multi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
import com.tangem.domain.tokens.model.NetworkStatus
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEmpty
import timber.log.Timber
/**
* Default implementation of [MultiNetworkStatusProducer]
*
* @property params params
* @property networksStatusesStore networks statuses store
* @property userWalletsStore user wallets store
* @property excludedBlockchains excluded blockchains
*/
internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor(
@Assisted val params: MultiNetworkStatusProducer.Params,
private val networksStatusesStore: NetworksStatusesStoreV2,
private val userWalletsStore: UserWalletsStore,
private val excludedBlockchains: ExcludedBlockchains,
) : MultiNetworkStatusProducer {
override val fallback: Set<NetworkStatus>
get() = setOf()
override fun produce(): Flow<Set<NetworkStatus>> {
return networksStatusesStore
.get(userWalletId = params.userWalletId)
.mapNotNull { statuses ->
val userWallet = userWalletsStore.getSyncOrNull(params.userWalletId)
if (userWallet == null) {
Timber.e("Unable to get UserWallet with provided ID: ${params.userWalletId}")
return@mapNotNull null
}
statuses.mapNotNullTo(hashSetOf()) { status ->
val network = getNetwork(
networkId = status.id.networkId,
derivationPath = status.id.derivationPath,
scanResponse = userWallet.scanResponse,
excludedBlockchains = excludedBlockchains,
) ?: return@mapNotNullTo null
NetworkStatus(network = network, value = status.value)
}
}
.distinctUntilChanged()
.onEmpty { emit(value = hashSetOf()) }
}
@AssistedFactory
interface Factory : MultiNetworkStatusProducer.Factory {
override fun create(params: MultiNetworkStatusProducer.Params): DefaultMultiNetworkStatusProducer
}
}

View file

@ -0,0 +1,273 @@
package com.tangem.data.networks.multi
import com.google.common.truth.Truth
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.network.MockNetworkStatusFactory
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.networks.models.SimpleNetworkStatus
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.networks.toSimple
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
import com.tangem.domain.tokens.model.NetworkStatus
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class DefaultMultiNetworkStatusProducerTest {
private val params = MultiNetworkStatusProducer.Params(userWalletId = userWallet.walletId)
private val networksStatusesStore = mockk<NetworksStatusesStoreV2>()
private val userWalletsStore = mockk<UserWalletsStore>()
private val excludedBlockchains = mockk<ExcludedBlockchains>()
private val producer = DefaultMultiNetworkStatusProducer(
params = params,
networksStatusesStore = networksStatusesStore,
userWalletsStore = userWalletsStore,
excludedBlockchains = excludedBlockchains,
)
@Before
fun setup() {
every { excludedBlockchains.contains(any()) } returns false
}
@Test
fun `test that flow is mapped for user wallet id from params`() = runTest {
val statuses = setOf(
MockNetworkStatusFactory.createVerified(ethNetwork),
MockNetworkStatusFactory.createVerified(cardanoNetwork),
)
val networksStatusesFlow = flowOf(statuses.map(NetworkStatus::toSimple).toSet())
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
val actual = producer.produce()
// check after producer.produce()
verify { networksStatusesStore.get(params.userWalletId) }
val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual)
// Check after flow was observed by subscriber (getEmittedValues).
// Otherwise, userWalletsStore.getSyncOrNull is not called.
verify { userWalletsStore.getSyncOrNull(params.userWalletId) }
Truth.assertThat(values.size).isEqualTo(1)
Truth.assertThat(values.first()).isEqualTo(statuses)
}
@Test
fun `test that flow is updated if statuses are updated`() = runTest {
val networksStatusesFlow = MutableSharedFlow<Set<SimpleNetworkStatus>>(replay = 2)
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
val actual = producer.produce()
// check after producer.produce()
verify { networksStatusesStore.get(params.userWalletId) }
// first emit
val statuses = setOf(
MockNetworkStatusFactory.createUnreachable(ethNetwork),
MockNetworkStatusFactory.createUnreachable(cardanoNetwork),
)
networksStatusesFlow.emit(statuses.map(NetworkStatus::toSimple).toSet())
val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual)
// Check after flow was observed by subscriber (getEmittedValues).
// Otherwise, userWalletsStore.getSyncOrNull is not called.
verify { userWalletsStore.getSyncOrNull(params.userWalletId) }
Truth.assertThat(values1.size).isEqualTo(1)
Truth.assertThat(values1.first()).isEqualTo(statuses)
// second emit
val updatedStatuses = setOf(
MockNetworkStatusFactory.createVerified(ethNetwork),
MockNetworkStatusFactory.createVerified(cardanoNetwork),
)
networksStatusesFlow.emit(updatedStatuses.map(NetworkStatus::toSimple).toSet())
val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual)
// Check after flow was observed by subscriber (getEmittedValues).
// Otherwise, userWalletsStore.getSyncOrNull is not called.
verify { userWalletsStore.getSyncOrNull(params.userWalletId) }
val expected = listOf(statuses, updatedStatuses)
Truth.assertThat(values2.size).isEqualTo(2)
Truth.assertThat(values2).isEqualTo(expected)
}
@Test
fun `test that flow is filtered the same status`() = runTest {
val networksStatusesFlow = MutableSharedFlow<Set<SimpleNetworkStatus>>(replay = 2)
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
val actual = producer.produce()
// check after producer.produce()
verify { networksStatusesStore.get(params.userWalletId) }
// first emit
val statuses = setOf(
MockNetworkStatusFactory.createUnreachable(ethNetwork),
MockNetworkStatusFactory.createUnreachable(cardanoNetwork),
)
networksStatusesFlow.emit(statuses.map(NetworkStatus::toSimple).toSet())
val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual)
// Check after flow was observed by subscriber (getEmittedValues).
// Otherwise, userWalletsStore.getSyncOrNull is not called.
verify { userWalletsStore.getSyncOrNull(params.userWalletId) }
Truth.assertThat(values1.size).isEqualTo(1)
Truth.assertThat(values1.first()).isEqualTo(statuses)
// second emit
networksStatusesFlow.emit(statuses.map(NetworkStatus::toSimple).toSet())
val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual)
// Check after flow was observed by subscriber (getEmittedValues).
// Otherwise, userWalletsStore.getSyncOrNull is not called.
verify { userWalletsStore.getSyncOrNull(params.userWalletId) }
Truth.assertThat(values2.size).isEqualTo(1)
Truth.assertThat(values2.first()).isEqualTo(statuses)
}
@Test
fun `test if flow throws exception`() = runTest {
val exception = IllegalStateException()
val statuses = setOf(
MockNetworkStatusFactory.createUnreachable(ethNetwork),
MockNetworkStatusFactory.createUnreachable(cardanoNetwork),
)
val innerFlow = MutableStateFlow(value = false)
val networksStatusesFlow = flow {
if (innerFlow.value) {
emit(statuses.map(NetworkStatus::toSimple).toSet())
} else {
throw exception
}
}
.buffer(capacity = 5)
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
val actual = producer.produceWithFallback()
// check after producer.produce()
verify { networksStatusesStore.get(params.userWalletId) }
val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual)
// Check after flow was observed by subscriber (getEmittedValues).
// Otherwise, userWalletsStore.getSyncOrNull is not called.
verify(inverse = true) { userWalletsStore.getSyncOrNull(params.userWalletId) }
Truth.assertThat(values1.size).isEqualTo(1)
Truth.assertThat(values1).isEqualTo(listOf(emptySet<NetworkStatus>()))
innerFlow.emit(value = true)
val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual)
// Check after flow was observed by subscriber (getEmittedValues).
// Otherwise, userWalletsStore.getSyncOrNull is not called.
verify { userWalletsStore.getSyncOrNull(params.userWalletId) }
Truth.assertThat(values2.size).isEqualTo(1)
Truth.assertThat(values2).isEqualTo(listOf(statuses))
}
@Test
fun `test that flow is empty`() = runTest {
every { networksStatusesStore.get(params.userWalletId) } returns emptyFlow()
every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
val actual = producer.produce()
// check after producer.produce()
verify { networksStatusesStore.get(params.userWalletId) }
val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual)
verify(inverse = true) { userWalletsStore.getSyncOrNull(params.userWalletId) }
Truth.assertThat(values.size).isEqualTo(1)
Truth.assertThat(values).isEqualTo(listOf(emptySet<NetworkStatus>()))
}
@Test
fun `test if network is null`() = runTest {
val statuses = setOf(
MockNetworkStatusFactory.createVerified(ethNetwork),
MockNetworkStatusFactory.createVerified(cardanoNetwork),
)
val networksStatusesFlow = flowOf(statuses.map(NetworkStatus::toSimple).toSet())
every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow
every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
every { excludedBlockchains.contains(any()) } returns true // cause network is null
val actual = producer.produce()
// check after producer.produce()
verify { networksStatusesStore.get(params.userWalletId) }
val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual)
// Check after flow was observed by subscriber (getEmittedValues).
// Otherwise, userWalletsStore.getSyncOrNull is not called.
verify { userWalletsStore.getSyncOrNull(params.userWalletId) }
Truth.assertThat(values.size).isEqualTo(1)
Truth.assertThat(values).isEqualTo(listOf(emptySet<NetworkStatus>()))
}
private companion object {
val scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(2),
derivedKeys = emptyMap(),
)
val userWallet = MockUserWalletFactory.create(scanResponse = scanResponse)
val ethNetwork = MockCryptoCurrencyFactory(scanResponse = scanResponse).ethereum.network.copy(
canHandleTokens = true,
)
val cardanoNetwork = MockCryptoCurrencyFactory(scanResponse = scanResponse).cardano.network
}
}

View file

@ -0,0 +1,10 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
api(projects.domain.core)
api(projects.domain.tokens.models)
api(projects.domain.wallets.models)
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.networks.multi
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
/**
* Producer of all networks statuses for wallet with [UserWalletId]
*
[REDACTED_AUTHOR]
*/
interface MultiNetworkStatusProducer : FlowProducer<Set<NetworkStatus>> {
data class Params(val userWalletId: UserWalletId)
interface Factory : FlowProducer.Factory<Params, MultiNetworkStatusProducer>
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.networks.multi
import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.tokens.model.NetworkStatus
/**
* Supplier of all networks statuses for selected wallet [MultiNetworkStatusProducer.Params]
*
* @property factory factory for creating [MultiNetworkStatusProducer]
* @property keyCreator key creator
*
[REDACTED_AUTHOR]
*/
abstract class MultiNetworkStatusSupplier(
override val factory: MultiNetworkStatusProducer.Factory,
override val keyCreator: (MultiNetworkStatusProducer.Params) -> String,
) : FlowCachingSupplier<MultiNetworkStatusProducer, MultiNetworkStatusProducer.Params, Set<NetworkStatus>>()

View file

@ -286,6 +286,7 @@ include(":domain:promo")
include(":domain:promo:models")
include(":domain:nft")
include(":domain:nft:models")
include(":domain:networks")
// endregion Domain modules
// region Data modules