Updated on 2026-08-14
This commit is contained in:
parent
c38c185a2d
commit
732df0ec4f
11 changed files with 430 additions and 6 deletions
|
|
@ -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>? {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>?
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
10
domain/networks/build.gradle.kts
Normal file
10
domain/networks/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>>()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue