Updated on 2026-08-14
This commit is contained in:
parent
d594af91fc
commit
c3f5ea4284
10 changed files with 496 additions and 43 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
import com.tangem.domain.blockaid.BlockAidGasEstimate
|
import com.tangem.domain.blockaid.BlockAidGasEstimate
|
||||||
|
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||||
import com.tangem.domain.quotes.QuotesRepository
|
import com.tangem.domain.quotes.QuotesRepository
|
||||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
import com.tangem.domain.transaction.FeeRepository
|
import com.tangem.domain.transaction.FeeRepository
|
||||||
|
|
@ -14,6 +15,8 @@ import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
|
|
@ -234,4 +237,18 @@ internal object YieldSupplyDomainModule {
|
||||||
): YieldSupplyGetAvailabilityUseCase {
|
): YieldSupplyGetAvailabilityUseCase {
|
||||||
return YieldSupplyGetAvailabilityUseCase(yieldSupplyRepository)
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.preferences.utils.store
|
||||||
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
|
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
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.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||||
|
|
@ -137,13 +136,10 @@ internal class DefaultYieldSupplyRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPendingTxHashes(
|
override suspend fun getPendingTxHashes(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): List<String> {
|
||||||
userWalletId: UserWalletId,
|
|
||||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
|
||||||
): List<String> {
|
|
||||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
network = cryptoCurrencyStatus.currency.network,
|
network = cryptoCurrency.network,
|
||||||
) ?: return emptyList()
|
) ?: return emptyList()
|
||||||
|
|
||||||
return walletManager.wallet.recentTransactions
|
return walletManager.wallet.recentTransactions
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.tangem.domain.yield.supply
|
package com.tangem.domain.yield.supply
|
||||||
|
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
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.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.yield.supply.models.YieldMarketToken
|
import com.tangem.domain.yield.supply.models.YieldMarketToken
|
||||||
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
|
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.
|
* Retrieve the hashes of pending (unconfirmed) transactions for the given wallet and currency.
|
||||||
*
|
*
|
||||||
* @param userWalletId the wallet to query
|
* @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
|
* @return list of transaction hashes that are still unconfirmed, or an empty list if none exist
|
||||||
*/
|
*/
|
||||||
suspend fun getPendingTxHashes(
|
suspend fun getPendingTxHashes(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): List<String>
|
||||||
userWalletId: UserWalletId,
|
|
||||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
|
||||||
): List<String>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last saved user‑initiated yield protocol action for the given wallet and currency, if any.
|
* Get the last saved user‑initiated yield protocol action for the given wallet and currency, if any.
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class YieldSupplyEnterStatusUseCase(
|
||||||
cryptoCurrencyStatus.currency,
|
cryptoCurrencyStatus.currency,
|
||||||
)
|
)
|
||||||
val pendingTxHashes = yieldSupplyRepository
|
val pendingTxHashes = yieldSupplyRepository
|
||||||
.getPendingTxHashes(userWalletId, cryptoCurrencyStatus)
|
.getPendingTxHashes(userWalletId, cryptoCurrencyStatus.currency)
|
||||||
.toSet()
|
.toSet()
|
||||||
val hasPendingTx = status?.txIds?.any { it in pendingTxHashes } == true
|
val hasPendingTx = status?.txIds?.any { it in pendingTxHashes } == true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<String>,
|
||||||
|
val attempts: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val trackedEntries = ConcurrentHashMap<TrackedKey, TrackedEntry>()
|
||||||
|
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<String>) {
|
||||||
|
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<SingleNetworkStatusFetcher.Params>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,7 +45,7 @@ class YieldSupplyEnterStatusUseCaseTest {
|
||||||
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
||||||
} returns status
|
} returns status
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
|
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency)
|
||||||
} returns listOf(pendingTxHash)
|
} returns listOf(pendingTxHash)
|
||||||
|
|
||||||
val result = useCase(userWalletId, cryptoStatus)
|
val result = useCase(userWalletId, cryptoStatus)
|
||||||
|
|
@ -65,7 +65,7 @@ class YieldSupplyEnterStatusUseCaseTest {
|
||||||
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
||||||
} returns status
|
} returns status
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
|
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency)
|
||||||
} returns listOf("0xdifferent")
|
} returns listOf("0xdifferent")
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null)
|
yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null)
|
||||||
|
|
@ -88,7 +88,7 @@ class YieldSupplyEnterStatusUseCaseTest {
|
||||||
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
||||||
} returns null
|
} returns null
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
|
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency)
|
||||||
} returns emptyList()
|
} returns emptyList()
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null)
|
yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null)
|
||||||
|
|
@ -111,7 +111,7 @@ class YieldSupplyEnterStatusUseCaseTest {
|
||||||
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
||||||
} returns status
|
} returns status
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
|
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency)
|
||||||
} returns emptyList()
|
} returns emptyList()
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null)
|
yieldSupplyRepository.saveTokenProtocolPendingStatus(userWalletId, token, null)
|
||||||
|
|
@ -153,7 +153,7 @@ class YieldSupplyEnterStatusUseCaseTest {
|
||||||
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
||||||
} returns status
|
} returns status
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
|
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency)
|
||||||
} returns listOf(pendingTxHash)
|
} returns listOf(pendingTxHash)
|
||||||
|
|
||||||
val result = useCase(userWalletId, cryptoStatus)
|
val result = useCase(userWalletId, cryptoStatus)
|
||||||
|
|
@ -174,7 +174,7 @@ class YieldSupplyEnterStatusUseCaseTest {
|
||||||
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
yieldSupplyRepository.getTokenProtocolPendingStatus(userWalletId, token)
|
||||||
} returns status
|
} returns status
|
||||||
coEvery {
|
coEvery {
|
||||||
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus)
|
yieldSupplyRepository.getPendingTxHashes(userWalletId, cryptoStatus.currency)
|
||||||
} returns listOf(matchingTxHash)
|
} returns listOf(matchingTxHash)
|
||||||
|
|
||||||
val result = useCase(userWalletId, cryptoStatus)
|
val result = useCase(userWalletId, cryptoStatus)
|
||||||
|
|
|
||||||
|
|
@ -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<SingleNetworkStatusFetcher.Params>()
|
||||||
|
|
||||||
|
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<SingleNetworkStatusFetcher.Params>()
|
||||||
|
|
||||||
|
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<SingleNetworkStatusFetcher.Params>()
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.entity.YieldSupplyUM
|
||||||
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer
|
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.coroutines.DelayedWork
|
|
||||||
import com.tangem.utils.coroutines.JobHolder
|
import com.tangem.utils.coroutines.JobHolder
|
||||||
import com.tangem.utils.coroutines.saveIn
|
import com.tangem.utils.coroutines.saveIn
|
||||||
import com.tangem.utils.transformer.update
|
import com.tangem.utils.transformer.update
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -55,7 +52,6 @@ internal class YieldSupplyModel @Inject constructor(
|
||||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||||
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||||
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||||
@DelayedWork private val coroutineScope: CoroutineScope,
|
|
||||||
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
|
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
|
||||||
private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase,
|
private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase,
|
||||||
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
|
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
|
||||||
|
|
@ -75,7 +71,6 @@ internal class YieldSupplyModel @Inject constructor(
|
||||||
var userWallet: UserWallet by Delegates.notNull()
|
var userWallet: UserWallet by Delegates.notNull()
|
||||||
private var latestCryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
private var latestCryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
||||||
|
|
||||||
private val fetchCurrencyJobHolder = JobHolder()
|
|
||||||
private val loadStatusJobHolder = JobHolder()
|
private val loadStatusJobHolder = JobHolder()
|
||||||
|
|
||||||
private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true)
|
private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true)
|
||||||
|
|
@ -201,7 +196,6 @@ internal class YieldSupplyModel @Inject constructor(
|
||||||
is YieldSupplyPendingStatus.Exit -> YieldSupplyUM.Processing.Exit
|
is YieldSupplyPendingStatus.Exit -> YieldSupplyUM.Processing.Exit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchCurrencyWithDelay()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
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(
|
private suspend fun loadActiveState(
|
||||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||||
yieldSupplyStatus: YieldSupplyStatus,
|
yieldSupplyStatus: YieldSupplyStatus,
|
||||||
|
|
@ -334,8 +314,4 @@ internal class YieldSupplyModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val PROCESSING_UPDATE_DELAY = 10_000L
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +65,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
|
||||||
private val yieldSupplyGetMaxFeeUseCase: YieldSupplyGetMaxFeeUseCase,
|
private val yieldSupplyGetMaxFeeUseCase: YieldSupplyGetMaxFeeUseCase,
|
||||||
private val yieldSupplyGetCurrentFeeUseCase: YieldSupplyGetCurrentFeeUseCase,
|
private val yieldSupplyGetCurrentFeeUseCase: YieldSupplyGetCurrentFeeUseCase,
|
||||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||||
|
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
|
||||||
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
|
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
|
||||||
|
|
||||||
private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require()
|
private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require()
|
||||||
|
|
@ -291,6 +292,11 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
|
yieldSupplyPendingTracker.addPending(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
txIds = txsData,
|
||||||
|
)
|
||||||
params.callback.onTransactionSent()
|
params.callback.onTransactionSent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||||
import com.tangem.domain.yield.supply.increaseGasLimitBy
|
import com.tangem.domain.yield.supply.increaseGasLimitBy
|
||||||
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
|
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
|
||||||
import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase
|
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.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase
|
||||||
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
|
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
|
||||||
import com.tangem.features.yield.supply.impl.R
|
import com.tangem.features.yield.supply.impl.R
|
||||||
|
|
@ -61,6 +62,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
|
||||||
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
|
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
|
||||||
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
|
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
|
||||||
private val yieldSupplyRepository: YieldSupplyRepository,
|
private val yieldSupplyRepository: YieldSupplyRepository,
|
||||||
|
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
|
||||||
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
|
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
|
||||||
|
|
||||||
private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require()
|
private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require()
|
||||||
|
|
@ -203,6 +205,11 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
|
yieldSupplyPendingTracker.addPending(
|
||||||
|
userWalletId = userWallet.walletId,
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
txIds = listOf(txId),
|
||||||
|
)
|
||||||
params.callback.onStopEarningTransactionSent()
|
params.callback.onStopEarningTransactionSent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue