From c3f5ea4284208c5cdcfca5461b07364410ec6246 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 22 Jan 2026 11:42:23 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/YieldSupplyDomainModule.kt | 17 + .../supply/DefaultYieldSupplyRepository.kt | 8 +- .../yield/supply/YieldSupplyRepository.kt | 8 +- .../usecase/YieldSupplyEnterStatusUseCase.kt | 2 +- .../usecase/YieldSupplyPendingTracker.kt | 147 +++++++++ .../YieldSupplyEnterStatusUseCaseTest.kt | 12 +- .../usecase/YieldSupplyPendingTrackerTest.kt | 308 ++++++++++++++++++ .../impl/main/model/YieldSupplyModel.kt | 24 -- .../model/YieldSupplyStartEarningModel.kt | 6 + .../model/YieldSupplyStopEarningModel.kt | 7 + 10 files changed, 496 insertions(+), 43 deletions(-) create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt create mode 100644 domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTrackerTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index 3301b9c21a..d74844c082 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.blockaid.BlockAidGasEstimate +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.FeeRepository @@ -14,6 +15,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Suppress("TooManyFunctions") @@ -234,4 +237,18 @@ internal object YieldSupplyDomainModule { ): YieldSupplyGetAvailabilityUseCase { return YieldSupplyGetAvailabilityUseCase(yieldSupplyRepository) } + + @Provides + @Singleton + fun provideYieldSupplyPendingProcessorUseCase( + yieldSupplyRepository: YieldSupplyRepository, + singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + dispatcherProvider: CoroutineDispatcherProvider, + ): YieldSupplyPendingTracker { + return YieldSupplyPendingTracker( + yieldSupplyRepository = yieldSupplyRepository, + singleNetworkStatusFetcher = singleNetworkStatusFetcher, + coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.io), + ) + } } \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index a2f8f3700f..3565a65952 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -19,7 +19,6 @@ import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.yield.supply.YieldSupplyRepository @@ -137,13 +136,10 @@ internal class DefaultYieldSupplyRepository( } } - override suspend fun getPendingTxHashes( - userWalletId: UserWalletId, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): List { + override suspend fun getPendingTxHashes(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): List { val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, + network = cryptoCurrency.network, ) ?: return emptyList() return walletManager.wallet.recentTransactions diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt index 7e371b0a14..c8841c32c5 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt @@ -1,7 +1,6 @@ package com.tangem.domain.yield.supply import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus @@ -84,13 +83,10 @@ interface YieldSupplyRepository { * Retrieve the hashes of pending (unconfirmed) transactions for the given wallet and currency. * * @param userWalletId the wallet to query - * @param cryptoCurrencyStatus the currency status containing network information + * @param cryptoCurrency the currency containing network information * @return list of transaction hashes that are still unconfirmed, or an empty list if none exist */ - suspend fun getPendingTxHashes( - userWalletId: UserWalletId, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): List + suspend fun getPendingTxHashes(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): List /** * Get the last saved user‑initiated yield protocol action for the given wallet and currency, if any. diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCase.kt index 726f1cd3df..271e08be2a 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCase.kt @@ -20,7 +20,7 @@ class YieldSupplyEnterStatusUseCase( cryptoCurrencyStatus.currency, ) val pendingTxHashes = yieldSupplyRepository - .getPendingTxHashes(userWalletId, cryptoCurrencyStatus) + .getPendingTxHashes(userWalletId, cryptoCurrencyStatus.currency) .toSet() val hasPendingTx = status?.txIds?.any { it in pendingTxHashes } == true diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt new file mode 100644 index 0000000000..2b2068de6c --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt @@ -0,0 +1,147 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.yield.supply.YieldSupplyRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap + +/** + * Periodically checks pending Yield Supply transaction hashes for a given set of wallet/currency pairs. + * + * - Call [addPending] right after sending a transaction to start tracking its hashes. + * - The checker runs every [CHECK_INTERVAL_MS] and queries repository for the current pending tx hashes. + * - If none of the stored hashes are pending anymore for a tracked entry, it triggers a single-network status refresh + * via [singleNetworkStatusFetcher] and removes the entry from tracking. + * - The periodic job stops automatically when there is nothing left to track. + */ +class YieldSupplyPendingTracker( + private val yieldSupplyRepository: YieldSupplyRepository, + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + private val coroutineScope: CoroutineScope, +) { + + private data class TrackedKey( + val userWalletId: UserWalletId, + val cryptoCurrencyId: CryptoCurrency.ID, + ) + + private data class TrackedEntry( + val cryptoCurrency: CryptoCurrency, + val txIds: Set, + val attempts: Int = 0, + ) + + private val trackedEntries = ConcurrentHashMap() + private val mutex = Mutex() + private var checkingJob: Job? = null + + /** + * Add tx ids to track for a wallet/currency pair. Starts periodic checking if needed. + */ + suspend fun addPending(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, txIds: List) { + val key = TrackedKey(userWalletId, cryptoCurrency.id) + trackedEntries.compute(key) { _, existing -> + if (existing == null) { + TrackedEntry( + cryptoCurrency = cryptoCurrency, + txIds = txIds.toSet(), + ) + } else { + existing.copy(txIds = existing.txIds + txIds) + } + } + + startAutomaticCheckingIfNeeded() + } + + private suspend fun startAutomaticCheckingIfNeeded() { + mutex.withLock { + if (checkingJob?.isActive == true) return + + checkingJob = coroutineScope.launch { + while (isActive) { + try { + checkAllTracked() + } catch (ex: Exception) { + Timber.e(ex) + } + delay(CHECK_INTERVAL_MS) + } + } + } + } + + private suspend fun stopAutomaticCheckingIfEmpty() { + if (trackedEntries.isEmpty()) { + mutex.withLock { + if (trackedEntries.isEmpty()) { + checkingJob?.cancel() + checkingJob = null + } + } + } + } + + private suspend fun checkAllTracked() { + val keysSnapshot = trackedEntries.keys().toList() + + if (keysSnapshot.isEmpty()) { + stopAutomaticCheckingIfEmpty() + return + } + + val networksToFetch = mutableSetOf() + + for (key in keysSnapshot) { + val entry = trackedEntries[key] ?: continue + if (entry.txIds.isEmpty()) { + trackedEntries.remove(key) + continue + } + + val pendingTxHashes = + yieldSupplyRepository.getPendingTxHashes( + userWalletId = key.userWalletId, + cryptoCurrency = entry.cryptoCurrency, + ) + .toSet() + + val hasStillPending = entry.txIds.any { it in pendingTxHashes } + + if (hasStillPending) { + trackedEntries.computeIfPresent(key) { _, current -> + val newAttempts = current.attempts + 1 + if (newAttempts >= MAX_ATTEMPTS) null else current.copy(attempts = newAttempts) + } + } else { + trackedEntries.remove(key) + } + networksToFetch.add( + SingleNetworkStatusFetcher.Params( + userWalletId = key.userWalletId, + network = entry.cryptoCurrency.network, + ), + ) + } + + for (params in networksToFetch) { + singleNetworkStatusFetcher(params) + } + + stopAutomaticCheckingIfEmpty() + } + + private companion object { + private const val CHECK_INTERVAL_MS = 10_000L + private const val MAX_ATTEMPTS = 6 + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt index 54955b5312..3929c947ab 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt @@ -45,7 +45,7 @@ class YieldSupplyEnterStatusUseCaseTest { yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token) } returns status coEvery { - yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus) + yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency) } returns listOf(pendingTxHash) val result = useCase(userWalletId, cryptoStatus) @@ -65,7 +65,7 @@ class YieldSupplyEnterStatusUseCaseTest { yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token) } returns status coEvery { - yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus) + yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency) } returns listOf("0xdifferent") coEvery { yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null) @@ -88,7 +88,7 @@ class YieldSupplyEnterStatusUseCaseTest { yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token) } returns null coEvery { - yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus) + yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency) } returns emptyList() coEvery { yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null) @@ -111,7 +111,7 @@ class YieldSupplyEnterStatusUseCaseTest { yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token) } returns status coEvery { - yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus) + yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency) } returns emptyList() coEvery { yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null) @@ -153,7 +153,7 @@ class YieldSupplyEnterStatusUseCaseTest { yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token) } returns status coEvery { - yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus) + yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency) } returns listOf(pendingTxHash) val result = useCase(userWalletId, cryptoStatus) @@ -174,7 +174,7 @@ class YieldSupplyEnterStatusUseCaseTest { yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token) } returns status coEvery { - yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus) + yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency) } returns listOf(matchingTxHash) val result = useCase(userWalletId, cryptoStatus) diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTrackerTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTrackerTest.kt new file mode 100644 index 0000000000..986b720a55 --- /dev/null +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTrackerTest.kt @@ -0,0 +1,308 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.yield.supply.YieldSupplyRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class YieldSupplyPendingTrackerTest { + + private val yieldSupplyRepository: YieldSupplyRepository = mockk() + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk() + + private lateinit var testScope: TestScope + private lateinit var useCase: YieldSupplyPendingTracker + + private val userWalletId = UserWalletId("00") + + @BeforeEach + fun setUp() { + testScope = TestScope(StandardTestDispatcher()) + useCase = YieldSupplyPendingTracker( + yieldSupplyRepository = yieldSupplyRepository, + singleNetworkStatusFetcher = singleNetworkStatusFetcher, + coroutineScope = testScope, + ) + } + + @Test + fun `GIVEN new tx ids WHEN addPending THEN stores entry for tracking`() = runTest { + val token = createToken() + val txIds = listOf("0xabc123", "0xdef456") + + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } returns txIds + coEvery { + singleNetworkStatusFetcher.invoke(any()) + } returns Either.Right(Unit) + + useCase.addPending(userWalletId, token, txIds) + + testScope.advanceTimeBy(10_001L) + + coVerify { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } + } + + @Test + fun `GIVEN existing entry WHEN addPending THEN merges tx ids`() = runTest { + val token = createToken() + val firstTxIds = listOf("0xfirst") + val secondTxIds = listOf("0xsecond") + + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } returns firstTxIds + secondTxIds + coEvery { + singleNetworkStatusFetcher.invoke(any()) + } returns Either.Right(Unit) + + useCase.addPending(userWalletId, token, firstTxIds) + useCase.addPending(userWalletId, token, secondTxIds) + + testScope.advanceTimeBy(10_001L) + + coVerify { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } + } + + @Test + fun `GIVEN tx still pending WHEN addPending THEN triggers network status refresh`() = runTest { + val token = createToken() + val txIds = listOf("0xpending") + val paramsSlot = slot() + + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } returns txIds + coEvery { + singleNetworkStatusFetcher.invoke(capture(paramsSlot)) + } returns Either.Right(Unit) + + useCase.addPending(userWalletId, token, txIds) + + testScope.advanceTimeBy(10_001L) + + coVerify { + singleNetworkStatusFetcher.invoke(any()) + } + assertThat(paramsSlot.captured.userWalletId).isEqualTo(userWalletId) + assertThat(paramsSlot.captured.network).isEqualTo(token.network) + } + + @Test + fun `GIVEN tx no longer pending WHEN addPending THEN removes entry from tracking and refreshes network`() = runTest { + val token = createToken() + val txIds = listOf("0xconfirmed") + + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } returns emptyList() + coEvery { + singleNetworkStatusFetcher.invoke(any()) + } returns Either.Right(Unit) + + useCase.addPending(userWalletId, token, txIds) + + testScope.advanceTimeBy(10_001L) + + coVerify(exactly = 1) { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } + coVerify(exactly = 1) { + singleNetworkStatusFetcher.invoke(any()) + } + } + + @Test + fun `GIVEN max attempts reached WHEN addPending THEN stops tracking entry`() = runTest { + val token = createToken() + val txIds = listOf("0xstuck") + + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } returns txIds + coEvery { + singleNetworkStatusFetcher.invoke(any()) + } returns Either.Right(Unit) + + useCase.addPending(userWalletId, token, txIds) + + testScope.advanceTimeBy(10_001L * 6) + + coVerify(exactly = 6) { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } + + testScope.advanceTimeBy(10_001L) + + coVerify(exactly = 6) { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } + } + + @Test + fun `GIVEN multiple wallets WHEN addPending THEN tracks each separately`() = runTest { + val token = createToken() + val userWalletId1 = UserWalletId("00") + val userWalletId2 = UserWalletId("01") + val txIds1 = listOf("0xtx1") + val txIds2 = listOf("0xtx2") + + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId1, token) + } returns txIds1 + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId2, token) + } returns txIds2 + coEvery { + singleNetworkStatusFetcher.invoke(any()) + } returns Either.Right(Unit) + + useCase.addPending(userWalletId1, token, txIds1) + useCase.addPending(userWalletId2, token, txIds2) + + testScope.advanceTimeBy(10_001L) + + coVerify { + yieldSupplyRepository.getPendingTxHashes(userWalletId1, token) + yieldSupplyRepository.getPendingTxHashes(userWalletId2, token) + } + } + + @Test + fun `GIVEN multiple wallets with same network WHEN checkAllTracked THEN fetches network status once per unique pair`() = runTest { + val token = createToken() + val userWalletId1 = UserWalletId("00") + val userWalletId2 = UserWalletId("01") + val txIds1 = listOf("0xtx1") + val txIds2 = listOf("0xtx2") + val capturedParams = mutableListOf() + + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId1, token) + } returns txIds1 + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId2, token) + } returns txIds2 + coEvery { + singleNetworkStatusFetcher.invoke(capture(capturedParams)) + } returns Either.Right(Unit) + + useCase.addPending(userWalletId1, token, txIds1) + useCase.addPending(userWalletId2, token, txIds2) + + testScope.advanceTimeBy(1000) + + assertThat(capturedParams).hasSize(2) + assertThat(capturedParams.map { it.userWalletId }.toSet()) + .containsExactly(userWalletId1, userWalletId2) + assertThat(capturedParams.map { it.network }.toSet()) + .containsExactly(token.network) + } + + @Test + fun `GIVEN same wallet with different currencies WHEN addPending THEN tracks each currency separately`() = runTest { + val token1 = createToken(networkId = "ethereum", contractAddress = "0xToken1") + val token2 = createToken(networkId = "polygon", contractAddress = "0xToken2") + val txIds1 = listOf("0xtx1") + val txIds2 = listOf("0xtx2") + + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token1) + } returns txIds1 + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token2) + } returns txIds2 + coEvery { + singleNetworkStatusFetcher.invoke(any()) + } returns Either.Right(Unit) + + useCase.addPending(userWalletId, token1, txIds1) + useCase.addPending(userWalletId, token2, txIds2) + + testScope.advanceTimeBy(10_001L) + + coVerify { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token1) + yieldSupplyRepository.getPendingTxHashes(userWalletId, token2) + } + } + + @Test + fun `GIVEN partial tx match WHEN addPending THEN still triggers refresh`() = runTest { + val token = createToken() + val trackedTxIds = listOf("0xtracked1", "0xtracked2") + val pendingTxIds = listOf("0xtracked1") + val paramsSlot = slot() + + coEvery { + yieldSupplyRepository.getPendingTxHashes(userWalletId, token) + } returns pendingTxIds + coEvery { + singleNetworkStatusFetcher.invoke(capture(paramsSlot)) + } returns Either.Right(Unit) + + useCase.addPending(userWalletId, token, trackedTxIds) + + testScope.advanceTimeBy(10_001L) + + coVerify { + singleNetworkStatusFetcher.invoke(any()) + } + assertThat(paramsSlot.captured.userWalletId).isEqualTo(userWalletId) + } + + private fun createToken( + networkId: String = "ethereum", + contractAddress: String = "0xToken", + ): CryptoCurrency.Token { + val derivationPath = Network.DerivationPath.None + val network = Network( + id = Network.ID(value = networkId, derivationPath = derivationPath), + backendId = networkId, + name = networkId, + currencySymbol = networkId.take(3).uppercase(), + derivationPath = derivationPath, + isTestnet = false, + standardType = Network.StandardType.Unspecified("UNSPECIFIED"), + hasFiatFeeRate = true, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + + return CryptoCurrency.Token( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(networkId), + suffix = CryptoCurrency.ID.Suffix.RawID(contractAddress), + ), + network = network, + name = "TEST_TOKEN", + symbol = "TTK", + decimals = 18, + iconUrl = null, + isCustom = false, + contractAddress = contractAddress, + ) + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index bbca4eb5ec..6aef4d48d4 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -31,12 +31,9 @@ import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.DelayedWork import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import com.tangem.utils.transformer.update -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -55,7 +52,6 @@ internal class YieldSupplyModel @Inject constructor( private val getUserWalletUseCase: GetUserWalletUseCase, private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - @DelayedWork private val coroutineScope: CoroutineScope, private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase, private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, @@ -75,7 +71,6 @@ internal class YieldSupplyModel @Inject constructor( var userWallet: UserWallet by Delegates.notNull() private var latestCryptoCurrencyStatus: CryptoCurrencyStatus? = null - private val fetchCurrencyJobHolder = JobHolder() private val loadStatusJobHolder = JobHolder() private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true) @@ -201,7 +196,6 @@ internal class YieldSupplyModel @Inject constructor( is YieldSupplyPendingStatus.Exit -> YieldSupplyUM.Processing.Exit } } - fetchCurrencyWithDelay() } private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { @@ -216,20 +210,6 @@ internal class YieldSupplyModel @Inject constructor( } } - private fun fetchCurrencyWithDelay() { - coroutineScope.launch(dispatchers.io) { - delay(PROCESSING_UPDATE_DELAY) - singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params( - userWalletId = userWallet.walletId, - network = cryptoCurrency.network, - ), - ).onLeft { - fetchCurrencyWithDelay() - } - }.saveIn(fetchCurrencyJobHolder) - } - private suspend fun loadActiveState( cryptoCurrencyStatus: CryptoCurrencyStatus, yieldSupplyStatus: YieldSupplyStatus, @@ -334,8 +314,4 @@ internal class YieldSupplyModel @Inject constructor( } } } - - private companion object { - const val PROCESSING_UPDATE_DELAY = 10_000L - } } \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index 144e0dc546..aae5996e1a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -65,6 +65,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( private val yieldSupplyGetMaxFeeUseCase: YieldSupplyGetMaxFeeUseCase, private val yieldSupplyGetCurrentFeeUseCase: YieldSupplyGetCurrentFeeUseCase, private val yieldSupplyRepository: YieldSupplyRepository, + private val yieldSupplyPendingTracker: YieldSupplyPendingTracker, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require() @@ -291,6 +292,11 @@ internal class YieldSupplyStartEarningModel @Inject constructor( } modelScope.launch { + yieldSupplyPendingTracker.addPending( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + txIds = txsData, + ) params.callback.onTransactionSent() } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index 3a0b8b4398..e79191dc86 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.increaseGasLimitBy import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyPendingTracker import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.impl.R @@ -61,6 +62,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, private val yieldSupplyRepository: YieldSupplyRepository, + private val yieldSupplyPendingTracker: YieldSupplyPendingTracker, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require() @@ -203,6 +205,11 @@ internal class YieldSupplyStopEarningModel @Inject constructor( } modelScope.launch { + yieldSupplyPendingTracker.addPending( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrency, + txIds = listOf(txId), + ) params.callback.onStopEarningTransactionSent() } }