From c9b99c587d075c4ffe7e0bd2f647f2f6ad2f6ccd Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Apr 2025 09:17:35 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../MockYieldBalanceWrapperDTOFactory.kt | 8 + .../multi/DefaultMultiYieldBalanceProducer.kt | 44 ++++ .../DefaultMultiYieldBalanceProducerTest.kt | 190 ++++++++++++++++++ .../multi/MultiYieldBalanceProducer.kt | 17 ++ .../multi/MultiYieldBalanceSupplier.kt | 18 ++ 5 files changed, 277 insertions(+) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt index 521604e487..71ad65f3d2 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt @@ -48,4 +48,12 @@ object MockYieldBalanceWrapperDTOFactory { integrationId = stakingId.integrationId, ) } + + fun createWithEmptyBalance(stakingId: StakingID = defaultStakingId): YieldBalanceWrapperDTO { + return YieldBalanceWrapperDTO( + addresses = Address(address = stakingId.address), + balances = emptyList(), + integrationId = stakingId.integrationId, + ) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt new file mode 100644 index 0000000000..533610817a --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt @@ -0,0 +1,44 @@ +package com.tangem.data.staking.multi + +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +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.flowOn +import kotlinx.coroutines.flow.onEmpty + +/** + * Default implementation of [MultiYieldBalanceProducer] + * + * @property params params + * @property yieldsBalancesStore yields balances store + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiYieldBalanceProducer @AssistedInject constructor( + @Assisted val params: MultiYieldBalanceProducer.Params, + private val yieldsBalancesStore: YieldsBalancesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : MultiYieldBalanceProducer { + + override val fallback: Set + get() = setOf() + + override fun produce(): Flow> { + return yieldsBalancesStore.get(userWalletId = params.userWalletId) + .distinctUntilChanged() + .onEmpty { emit(value = hashSetOf()) } + .flowOn(dispatchers.default) + } + + @AssistedFactory + interface Factory : MultiYieldBalanceProducer.Factory { + override fun create(params: MultiYieldBalanceProducer.Params): DefaultMultiYieldBalanceProducer + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt new file mode 100644 index 0000000000..a545c87d92 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt @@ -0,0 +1,190 @@ +package com.tangem.data.staking.multi + +import com.google.common.truth.Truth +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.toDomain +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiYieldBalanceProducerTest { + + private val params = MultiYieldBalanceProducer.Params(userWalletId = UserWalletId("011")) + + private val yieldsBalancesStore = mockk() + private val dispatchers = TestingCoroutineDispatcherProvider() + + private val producer = DefaultMultiYieldBalanceProducer( + params = params, + yieldsBalancesStore = yieldsBalancesStore, + dispatchers = dispatchers, + ) + + @Test + fun `test that flow is mapped for user wallet id from params`() = runTest { + val balances = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + + val networksStatusesFlow = flowOf(balances) + + every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values.first()).isEqualTo(balances) + } + + @Test + fun `test that flow is updated if balances are updated`() = runTest { + val networksStatusesFlow = MutableSharedFlow>(replay = 2) + + every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + // first emit + val balances = setOf( + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(), + ) + + networksStatusesFlow.emit(balances) + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(balances) + + // second emit + val updatedWrappers = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + + networksStatusesFlow.emit(updatedWrappers) + + val values2 = getEmittedValues(flow = actual) + + val expected = listOf(balances, updatedWrappers) + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2).isEqualTo(expected) + } + + @Test + fun `test that flow is filtered the same balance`() = runTest { + val networksStatusesFlow = MutableSharedFlow>(replay = 2) + + every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + // first emit + val wrappers = setOf( + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(), + ) + + networksStatusesFlow.emit(wrappers) + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(wrappers) + + // second emit + networksStatusesFlow.emit(wrappers) + + val values2 = getEmittedValues(flow = actual) + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2.first()).isEqualTo(wrappers) + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + val balances = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + + val innerFlow = MutableStateFlow(value = false) + val networksStatusesFlow = flow { + if (innerFlow.value) { + emit(balances) + } else { + throw exception + } + } + .buffer(capacity = 5) + + every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + + val actual = producer.produceWithFallback() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(emptySet())) + + innerFlow.emit(value = true) + + val values2 = getEmittedValues(flow = actual) + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(balances)) + } + + @Test + fun `test that flow is empty`() = runTest { + every { yieldsBalancesStore.get(params.userWalletId) } returns emptyFlow() + + val actual = producer.produce() + + // check after producer.produce() + verify { yieldsBalancesStore.get(params.userWalletId) } + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) + } + + private companion object { + + val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId + val solanaId = YieldsBalancesStore.StakingID( + integrationId = "solana-sol-native-multivalidator-staking", + address = "0x1", + ) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt new file mode 100644 index 0000000000..eaaff5ad56 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.staking.multi + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Producer of all yield balances for selected wallet [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface MultiYieldBalanceProducer : FlowProducer> { + + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt new file mode 100644 index 0000000000..4c6cbed1f3 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking.multi + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.staking.model.stakekit.YieldBalance + +/** + * Supplier of all yield balances for selected wallet [MultiYieldBalanceProducer.Params] + * + * @property factory factory for creating [MultiYieldBalanceProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class MultiYieldBalanceSupplier( + override val factory: FlowProducer.Factory, + override val keyCreator: (MultiYieldBalanceProducer.Params) -> String, +) : FlowCachingSupplier>() \ No newline at end of file