Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-27 13:43:33 +04:00
parent fcb3b772e4
commit 983b9b253d
7 changed files with 224 additions and 54 deletions

View file

@ -502,7 +502,7 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): YieldBalance {
val stakingId = stakingIdFactory.createForDefault(
val stakingId = stakingIdFactory.create(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
network = cryptoCurrency.network,
@ -639,7 +639,7 @@ internal class DefaultStakingRepository(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): YieldBalanceList {
val stakingIds = cryptoCurrencies.flatMap {
val stakingIds = cryptoCurrencies.mapNotNull {
stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network)
}

View file

@ -93,7 +93,7 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
private suspend fun getStakingIds(params: MultiYieldBalanceFetcher.Params) = either {
val stakingIds = catch(
block = {
params.currencyIdWithNetworkMap.flatMapTo(hashSetOf()) { (currencyId, network) ->
params.currencyIdWithNetworkMap.mapNotNullTo(hashSetOf()) { (currencyId, network) ->
stakingIdFactory.create(
userWalletId = params.userWalletId,
currencyId = currencyId,

View file

@ -39,26 +39,24 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
)
}
private var stakingIds: Set<StakingID>? = null
private var stakingId: StakingID? = null
override fun produce(): Flow<YieldBalance> {
return multiYieldBalanceSupplier(
params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId),
)
.mapNotNull { balances ->
val currentStakingIds = getStakingIds().ifEmpty {
return@mapNotNull YieldBalance.Unsupported
}
val currentStakingId = getStakingId() ?: return@mapNotNull YieldBalance.Unsupported
balances.firstOrNull { currentStakingIds.contains(it.getStakingId()) }
balances.firstOrNull { it.getStakingId() == currentStakingId }
?: YieldBalance.Unsupported
}
.distinctUntilChanged()
.flowOn(dispatchers.default)
}
private suspend fun getStakingIds(): Set<StakingID> {
val saved = stakingIds
private suspend fun getStakingId(): StakingID? {
val saved = stakingId
if (saved != null) return saved
@ -67,7 +65,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
currencyId = params.currencyId,
network = params.network,
)
.also { stakingIds = it }
.also { stakingId = it }
}
@AssistedFactory

View file

@ -21,21 +21,7 @@ internal class StakingIdFactory @Inject constructor(
private val walletManagersFacade: WalletManagersFacade,
) {
suspend fun create(userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network): Set<StakingID> {
val addresses = walletManagersFacade.getAddresses(userWalletId = userWalletId, network = network)
val integrationId = createIntegrationId(currencyId) ?: return emptySet()
return addresses.mapTo(hashSetOf()) { address ->
StakingID(integrationId = integrationId, address = address.value)
}
}
suspend fun createForDefault(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
network: Network,
): StakingID? {
suspend fun create(userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network): StakingID? {
val address = walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network)
val integrationId = createIntegrationId(currencyId)

View file

@ -60,8 +60,8 @@ internal class DefaultMultiYieldBalanceFetcherTest {
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId))
@ -102,8 +102,8 @@ internal class DefaultMultiYieldBalanceFetcherTest {
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
val yields = listOf(MockYieldDTOFactory.create(tonId))
@ -152,7 +152,7 @@ internal class DefaultMultiYieldBalanceFetcherTest {
coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) }
coVerify(inverse = true) {
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any())
stakingYieldsStore.getSyncWithTimeout()
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
@ -183,7 +183,7 @@ internal class DefaultMultiYieldBalanceFetcherTest {
coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) }
coVerify(inverse = true) {
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any())
stakingYieldsStore.getSyncWithTimeout()
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
@ -199,15 +199,15 @@ internal class DefaultMultiYieldBalanceFetcherTest {
}
@Test
fun `fetch yields balances failure if stakingIdFactory returns empty list`() = runTest {
fun `fetch yields balances failure if stakingIdFactory returns null`() = runTest {
// Arrange
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns emptySet()
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns emptySet()
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns null
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns null
// Actual
val actual = fetcher.invoke(params)
@ -242,8 +242,8 @@ internal class DefaultMultiYieldBalanceFetcherTest {
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
@ -281,8 +281,8 @@ internal class DefaultMultiYieldBalanceFetcherTest {
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList()
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
@ -320,8 +320,8 @@ internal class DefaultMultiYieldBalanceFetcherTest {
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
val yields = listOf(
@ -364,8 +364,8 @@ internal class DefaultMultiYieldBalanceFetcherTest {
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1")))
@ -411,8 +411,8 @@ internal class DefaultMultiYieldBalanceFetcherTest {
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId))

View file

@ -54,7 +54,7 @@ internal class DefaultSingleYieldBalanceProducerTest {
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
every { multiNetworkStatusSupplier(multiParams) } returns expected
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
val actual = producer.produce()
@ -74,7 +74,7 @@ internal class DefaultSingleYieldBalanceProducerTest {
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
every { multiNetworkStatusSupplier(multiParams) } returns expected
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
val actual = producer.produceWithFallback()
@ -109,7 +109,7 @@ internal class DefaultSingleYieldBalanceProducerTest {
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
every { multiNetworkStatusSupplier(multiParams) } returns expected
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
val actual = producer.produceWithFallback()
@ -168,7 +168,7 @@ internal class DefaultSingleYieldBalanceProducerTest {
val fallbackStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = null)
Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus))
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
innerFlow.emit(value = true)
@ -188,7 +188,7 @@ internal class DefaultSingleYieldBalanceProducerTest {
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId
val actual = producer.produce()
@ -203,14 +203,14 @@ internal class DefaultSingleYieldBalanceProducerTest {
}
@Test
fun `test if wallet manager facade returns empty set`() = runTest {
fun `test if wallet manager facade returns null`() = runTest {
val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain()
val yieldBalancesFlow = flowOf(setOf(balance))
val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId)
every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns emptySet()
coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns null
val actual = producer.produce()
@ -235,7 +235,5 @@ internal class DefaultSingleYieldBalanceProducerTest {
integrationId = "solana-sol-native-multivalidator-staking",
address = "0x1",
)
val stakingIds = setOf(tonId)
}
}

View file

@ -0,0 +1,188 @@
package com.tangem.data.staking.utils
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.StakingID
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class StakingIdFactoryTest {
private val walletManagersFacade: WalletManagersFacade = mockk()
private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade)
@BeforeEach
fun resetMocks() {
clearMocks(walletManagersFacade)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CreateIntegrationId {
@ParameterizedTest
@ProvideTestModels
fun createIntegrationId(model: CreateIntegrationIdModel) {
// Act
val actual = factory.createIntegrationId(currencyId = model.currencyId)
// Assert
Truth.assertThat(actual).isEqualTo(model.expected)
}
private fun provideTestModels() = listOf(
CreateIntegrationIdModel(
currencyId = createCurrencyId(blockchain = Blockchain.TON),
expected = "ton-ton-chorus-one-pools-staking",
),
CreateIntegrationIdModel(
currencyId = createCurrencyId(blockchain = Blockchain.Solana),
expected = "solana-sol-native-multivalidator-staking",
),
CreateIntegrationIdModel(
currencyId = createCurrencyId(blockchain = Blockchain.Cosmos),
expected = "cosmos-atom-native-staking",
),
CreateIntegrationIdModel(
currencyId = createCurrencyId(blockchain = Blockchain.Tron),
expected = "tron-trx-native-staking",
),
CreateIntegrationIdModel(
currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"),
expected = "ethereum-matic-native-staking",
),
CreateIntegrationIdModel(
currencyId = createCurrencyId(blockchain = Blockchain.BSC),
expected = "bsc-bnb-native-staking",
),
CreateIntegrationIdModel(
currencyId = createCurrencyId(blockchain = Blockchain.Cardano),
expected = "cardano-ada-native-staking",
),
CreateIntegrationIdModel(
currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin),
expected = null,
),
)
}
data class CreateIntegrationIdModel(val currencyId: CryptoCurrency.ID, val expected: String?)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Create {
private val defaultAddress = "address"
@Test
fun `create returns null if address is null`() = runTest {
// Arrange
val userWalletId = UserWalletId(stringValue = "011")
val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON)
coEvery {
walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network)
} returns null
// Act
val actual = factory.create(
userWalletId = userWalletId,
currencyId = currency.id,
network = currency.network,
)
// Assert
val expected = null
Truth.assertThat(actual).isEqualTo(expected)
coVerify(exactly = 1) {
walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network)
}
}
@ParameterizedTest
@ProvideTestModels
fun create(model: CreateModel) = runTest {
// Arrange
val userWalletId = UserWalletId(stringValue = "011")
val network = MockCryptoCurrencyFactory().createCoin(Blockchain.TON).network
coEvery {
walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network)
} returns defaultAddress
// Act
val actual = factory.create(userWalletId = userWalletId, currencyId = model.currencyId, network = network)
// Assert
Truth.assertThat(actual).isEqualTo(model.expected)
coVerify(exactly = 1) {
walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network)
}
}
private fun provideTestModels() = listOf(
CreateModel(
currencyId = createCurrencyId(blockchain = Blockchain.TON),
expected = createStakingId(integrationId = "ton-ton-chorus-one-pools-staking"),
),
CreateModel(
currencyId = createCurrencyId(blockchain = Blockchain.Solana),
expected = createStakingId(integrationId = "solana-sol-native-multivalidator-staking"),
),
CreateModel(
currencyId = createCurrencyId(blockchain = Blockchain.Cosmos),
expected = createStakingId(integrationId = "cosmos-atom-native-staking"),
),
CreateModel(
currencyId = createCurrencyId(blockchain = Blockchain.Tron),
expected = createStakingId(integrationId = "tron-trx-native-staking"),
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"),
expected = createStakingId(integrationId = "ethereum-matic-native-staking"),
),
CreateModel(
currencyId = createCurrencyId(blockchain = Blockchain.BSC),
expected = createStakingId(integrationId = "bsc-bnb-native-staking"),
),
CreateModel(
currencyId = createCurrencyId(blockchain = Blockchain.Cardano),
expected = createStakingId(integrationId = "cardano-ada-native-staking"),
),
CreateModel(
currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin),
expected = null,
),
)
private fun createStakingId(integrationId: String): StakingID {
return StakingID(integrationId = integrationId, address = defaultAddress)
}
}
data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: StakingID?)
private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID {
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}${blockchain.toCoinId()}")
}
}