Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-20 16:18:42 +05:00
parent 50f30e9bbf
commit 2a1369fce6
9 changed files with 223 additions and 0 deletions

View file

@ -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)

View file

@ -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,
)
}
}

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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<UserWalletId>
fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress>
fun acknowledgeCompletion(userWalletId: UserWalletId)
suspend fun clearPendingFlag(userWalletId: UserWalletId)
suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
suspend fun clearDiscoveredTokens(userWalletId: UserWalletId)
}

View file

@ -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)
}
}

View file

@ -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<TokenSyncProgress> {
return tokenSyncRepository.observeSyncProgress(userWalletId)
}
}

View file

@ -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<UserWalletId, Job>()
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<Throwable, Unit> = 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
},
)
}
}

View file

@ -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")