From 2a1369fce683ef7d5a8617d9c6515bbfbbfb1a41 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Mar 2026 16:18:42 +0500 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tap/di/domain/TokenSyncDomainModule.kt | 50 ++++++++++++ domain/tokensync/build.gradle.kts | 20 +++++ .../tokensync/model/TokenSyncProgress.kt | 22 ++++++ .../repository/TokenSyncRepository.kt | 23 ++++++ .../AcknowledgeTokenSyncCompletionUseCase.kt | 13 ++++ .../usecase/ObserveTokenSyncUseCase.kt | 15 ++++ .../tokensync/usecase/SyncTokensUseCase.kt | 78 +++++++++++++++++++ settings.gradle.kts | 1 + 9 files changed, 223 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt create mode 100644 domain/tokensync/build.gradle.kts create mode 100644 domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt create mode 100644 domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt create mode 100644 domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/AcknowledgeTokenSyncCompletionUseCase.kt create mode 100644 domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/ObserveTokenSyncUseCase.kt create mode 100644 domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 10ff946ef5..a2c8978746 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -160,6 +160,7 @@ dependencies { implementation(projects.domain.hotWallet) implementation(projects.domain.news) implementation(projects.domain.earn) + implementation(projects.domain.tokensync) implementation(projects.common) implementation(projects.common.routing) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt new file mode 100644 index 0000000000..890ef9b566 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase +import com.tangem.domain.tokensync.repository.TokenSyncRepository +import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase +import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase +import com.tangem.domain.tokensync.usecase.SyncTokensUseCase +import com.tangem.utils.coroutines.AppCoroutineScope +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TokenSyncDomainModule { + + @Provides + @Singleton + fun provideObserveTokenSyncUseCase(tokenSyncRepository: TokenSyncRepository): ObserveTokenSyncUseCase { + return ObserveTokenSyncUseCase( + tokenSyncRepository = tokenSyncRepository, + ) + } + + @Provides + @Singleton + fun provideAcknowledgeTokenSyncCompletionUseCase( + tokenSyncRepository: TokenSyncRepository, + ): AcknowledgeTokenSyncCompletionUseCase { + return AcknowledgeTokenSyncCompletionUseCase( + tokenSyncRepository = tokenSyncRepository, + ) + } + + @Provides + @Singleton + fun provideSyncTokensUseCase( + tokenSyncRepository: TokenSyncRepository, + manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, + appCoroutineScope: AppCoroutineScope, + ): SyncTokensUseCase { + return SyncTokensUseCase( + tokenSyncRepository = tokenSyncRepository, + manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase, + appCoroutineScope = appCoroutineScope, + ) + } +} \ No newline at end of file diff --git a/domain/tokensync/build.gradle.kts b/domain/tokensync/build.gradle.kts new file mode 100644 index 0000000000..bdf6721531 --- /dev/null +++ b/domain/tokensync/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.tokensync" +} + +dependencies { + api(projects.domain.core) + implementation(projects.domain.models) + implementation(projects.domain.account.status) + implementation(projects.core.utils) + + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + implementation(deps.timber) +} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt new file mode 100644 index 0000000000..f094aa655a --- /dev/null +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.tokensync.model + +sealed class TokenSyncProgress { + + data object Idle : TokenSyncProgress() + + data class InProgress( + val completedNetworks: Int, + val totalNetworks: Int, + ) : TokenSyncProgress() { + val progressPercent: Int + get() = if (totalNetworks > 0) { + completedNetworks * 100 / totalNetworks + } else { + 0 + } + } + + data object Completed : TokenSyncProgress() + + data class Error(val cause: Throwable) : TokenSyncProgress() +} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt new file mode 100644 index 0000000000..c6f2d19976 --- /dev/null +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.tokensync.repository + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokensync.model.TokenSyncProgress +import kotlinx.coroutines.flow.Flow + +interface TokenSyncRepository { + + suspend fun runSync(userWalletId: UserWalletId) + + suspend fun getPendingSyncWalletIds(): List + + fun observeSyncProgress(userWalletId: UserWalletId): Flow + + fun acknowledgeCompletion(userWalletId: UserWalletId) + + suspend fun clearPendingFlag(userWalletId: UserWalletId) + + suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List + + suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/AcknowledgeTokenSyncCompletionUseCase.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/AcknowledgeTokenSyncCompletionUseCase.kt new file mode 100644 index 0000000000..35be72c523 --- /dev/null +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/AcknowledgeTokenSyncCompletionUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.tokensync.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokensync.repository.TokenSyncRepository + +class AcknowledgeTokenSyncCompletionUseCase( + private val tokenSyncRepository: TokenSyncRepository, +) { + + operator fun invoke(userWalletId: UserWalletId) { + tokenSyncRepository.acknowledgeCompletion(userWalletId) + } +} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/ObserveTokenSyncUseCase.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/ObserveTokenSyncUseCase.kt new file mode 100644 index 0000000000..cddfbb9d7f --- /dev/null +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/ObserveTokenSyncUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.tokensync.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokensync.model.TokenSyncProgress +import com.tangem.domain.tokensync.repository.TokenSyncRepository +import kotlinx.coroutines.flow.Flow + +class ObserveTokenSyncUseCase( + private val tokenSyncRepository: TokenSyncRepository, +) { + + operator fun invoke(userWalletId: UserWalletId): Flow { + return tokenSyncRepository.observeSyncProgress(userWalletId) + } +} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt new file mode 100644 index 0000000000..d3f8fb0c31 --- /dev/null +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt @@ -0,0 +1,78 @@ +package com.tangem.domain.tokensync.usecase + +import arrow.core.Either +import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokensync.repository.TokenSyncRepository +import com.tangem.utils.coroutines.AppCoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap + +class SyncTokensUseCase( + private val tokenSyncRepository: TokenSyncRepository, + private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, + private val appCoroutineScope: AppCoroutineScope, +) { + + private val activeSyncJobs = ConcurrentHashMap() + + operator fun invoke(userWalletId: UserWalletId) { + activeSyncJobs[userWalletId]?.cancel() + activeSyncJobs[userWalletId] = appCoroutineScope.launch { + try { + tokenSyncRepository.runSync(userWalletId) + applyDiscoveredTokens(userWalletId) + } catch (e: Exception) { + Timber.e(e, "Token sync failed for wallet: $userWalletId") + } finally { + activeSyncJobs.remove(userWalletId) + } + } + } + + suspend fun cancel(userWalletId: UserWalletId): Either = Either.catch { + activeSyncJobs.remove(userWalletId)?.cancel() + tokenSyncRepository.clearPendingFlag(userWalletId) + tokenSyncRepository.clearDiscoveredTokens(userWalletId) + } + + fun applyPendingSyncs() { + appCoroutineScope.launch { + try { + val pendingIds = tokenSyncRepository.getPendingSyncWalletIds() + for (walletId in pendingIds) { + val isApplied = applyDiscoveredTokens(walletId) + if (isApplied) { + tokenSyncRepository.clearPendingFlag(walletId) + } + } + } catch (e: Exception) { + Timber.e(e, "Failed to apply pending syncs") + } + } + } + + private suspend fun applyDiscoveredTokens(userWalletId: UserWalletId): Boolean { + val currencies = tokenSyncRepository.getDiscoveredCurrencies(userWalletId) + + if (currencies.isEmpty()) return true + + val accountId = AccountId.forMainCryptoPortfolio(userWalletId) + return manageCryptoCurrenciesUseCase( + accountId = accountId, + add = currencies, + ).fold( + ifRight = { + tokenSyncRepository.clearDiscoveredTokens(userWalletId) + true + }, + ifLeft = { error -> + Timber.e("Failed to apply discovered tokens for wallet: $userWalletId, error: $error") + false + }, + ) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d5500335a1..4a3ebd13c0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -338,6 +338,7 @@ include(":domain:demo:models") include(":domain:settings") include(":domain:tokens") include(":domain:tokens:models") +include(":domain:tokensync") include(":domain:wallets") include(":domain:wallets:models") include(":domain:txhistory")