From 732df0ec4f799ec724f43a68f157ac84ba859938 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 17:00:02 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/data/RuntimeUserWalletsStore.kt | 11 +- common/test/build.gradle.kts | 2 + .../domain/wallet/MockUserWalletFactory.kt | 33 +++ .../local/userwallet/UserWalletsStore.kt | 4 +- data/networks/build.gradle.kts | 2 + .../DefaultMultiNetworkStatusProducer.kt | 66 +++++ .../DefaultMultiNetworkStatusProducerTest.kt | 273 ++++++++++++++++++ domain/networks/build.gradle.kts | 10 + .../multi/MultiNetworkStatusProducer.kt | 17 ++ .../multi/MultiNetworkStatusSupplier.kt | 17 ++ settings.gradle.kts | 1 + 11 files changed, 430 insertions(+), 6 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt create mode 100644 domain/networks/build.gradle.kts create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusProducer.kt create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index 1661cedb1d..9870501655 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -20,11 +20,12 @@ internal class RuntimeUserWalletsStore( override val userWallets: Flow> 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? { diff --git a/common/test/build.gradle.kts b/common/test/build.gradle.kts index 4fd8c4f361..00c1510373 100644 --- a/common/test/build.gradle.kts +++ b/common/test/build.gradle.kts @@ -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) diff --git a/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt new file mode 100644 index 0000000000..365267e1ba --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index 54fe4ff322..56acd23a53 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -11,7 +11,9 @@ interface UserWalletsStore { val userWallets: Flow> - suspend fun getSyncOrNull(key: UserWalletId): UserWallet? + fun getSyncOrNull(key: UserWalletId): UserWallet? + + suspend fun getSyncStrict(key: UserWalletId): UserWallet suspend fun getAllSyncOrNull(): List? diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index f74ca832ca..079242811e 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -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) diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt new file mode 100644 index 0000000000..2e060a8d75 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt @@ -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 + get() = setOf() + + override fun produce(): Flow> { + 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 + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt new file mode 100644 index 0000000000..ebed6000a6 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt @@ -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() + private val userWalletsStore = mockk() + private val excludedBlockchains = mockk() + + 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>(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>(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())) + + 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())) + } + + @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())) + } + + 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 + } +} \ No newline at end of file diff --git a/domain/networks/build.gradle.kts b/domain/networks/build.gradle.kts new file mode 100644 index 0000000000..8207889da2 --- /dev/null +++ b/domain/networks/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusProducer.kt b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusProducer.kt new file mode 100644 index 0000000000..eb47772afc --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusProducer.kt @@ -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> { + + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt new file mode 100644 index 0000000000..7ca732a0d2 --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt @@ -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>() \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a63804ffe8..b4efe097b4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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