diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2d10257dfe..ab2fe2e0b3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -189,6 +189,7 @@ dependencies { implementation(projects.data.common) implementation(projects.data.settings) implementation(projects.data.tokens) + implementation(projects.data.tokensync) implementation(projects.data.txhistory) implementation(projects.data.wallets) implementation(projects.data.analytics) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index fbcfc74ef6..ca5625c8b6 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -71,5 +71,9 @@ { "name": "VIRTUAL_ACCOUNTS_ENABLED", "version": "undefined" + }, + { + "name": "TOKEN_SYNC_ENABLED", + "version": "undefined" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 330762e864..739514fe49 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -28,6 +28,7 @@ interface TangemTechApi { suspend fun getCoins( @Header("Cache-Control") cacheControl: String = "max-age=600", @Query("contractAddress") contractAddress: String? = null, + @Query("contractAddresses") contractAddresses: String? = null, @Query("exchangeable") exchangeable: Boolean? = null, @Query("networkIds") networkIds: String? = null, @Query("networkId") networkId: String? = null, diff --git a/data/tokensync/build.gradle.kts b/data/tokensync/build.gradle.kts new file mode 100644 index 0000000000..5c9c29675d --- /dev/null +++ b/data/tokensync/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.tokensync" +} + +dependencies { + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.domain.models) + + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + implementation(deps.androidx.datastore) + implementation(deps.kotlin.coroutines) + implementation(deps.moshi) +} \ No newline at end of file diff --git a/data/tokensync/src/main/java/com/tangem/data/tokensync/store/DefaultTokenSyncStore.kt b/data/tokensync/src/main/java/com/tangem/data/tokensync/store/DefaultTokenSyncStore.kt new file mode 100644 index 0000000000..d498d28ca3 --- /dev/null +++ b/data/tokensync/src/main/java/com/tangem/data/tokensync/store/DefaultTokenSyncStore.kt @@ -0,0 +1,24 @@ +package com.tangem.data.tokensync.store + +import androidx.datastore.core.DataStore +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import kotlinx.coroutines.flow.firstOrNull + +internal class DefaultTokenSyncStore( + private val persistenceStore: DataStore>, +) : TokenSyncStore { + + override suspend fun get(): List { + return persistenceStore.data.firstOrNull().orEmpty() + } + + override suspend fun append(tokens: List) { + persistenceStore.updateData { existing -> + (existing + tokens).distinct() + } + } + + override suspend fun clear() { + persistenceStore.updateData { emptyList() } + } +} \ No newline at end of file diff --git a/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStore.kt b/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStore.kt new file mode 100644 index 0000000000..919b551121 --- /dev/null +++ b/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStore.kt @@ -0,0 +1,12 @@ +package com.tangem.data.tokensync.store + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse + +interface TokenSyncStore { + + suspend fun get(): List + + suspend fun append(tokens: List) + + suspend fun clear() +} \ No newline at end of file diff --git a/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStoreFactory.kt b/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStoreFactory.kt new file mode 100644 index 0000000000..43c191b61c --- /dev/null +++ b/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStoreFactory.kt @@ -0,0 +1,56 @@ +package com.tangem.data.tokensync.store + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.listTypes +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.AppCoroutineScope +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TokenSyncStoreFactory @Inject constructor( + @NetworkMoshi private val moshi: Moshi, + @ApplicationContext private val context: Context, + private val appScope: AppCoroutineScope, +) { + + private val stores = ConcurrentHashMap() + + fun provide(userWalletId: UserWalletId): TokenSyncStore { + val userWalletStringId = userWalletId.formatted() + return stores.computeIfAbsent(userWalletStringId) { + DefaultTokenSyncStore( + persistenceStore = createPersistenceStore( + fileName = "token_sync_$userWalletStringId", + types = listTypes(), + defaultValue = emptyList(), + ), + ) + } + } + + private fun createPersistenceStore( + fileName: String, + types: java.lang.reflect.ParameterizedType, + defaultValue: T, + ): DataStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = types, + defaultValue = defaultValue, + ), + produceFile = { context.dataStoreFile(fileName = fileName) }, + scope = appScope, + ) + + private fun UserWalletId.formatted(): String = stringValue.lowercase() +} \ No newline at end of file diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index d10812bf6b..5791e4e9e0 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -18,6 +18,7 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.nft.models.NFTAsset import com.tangem.blockchain.nft.models.NFTCollection +import com.tangem.blockchain.tokenbalance.models.TokenBalance import com.tangem.blockchain.transactionhistory.models.TransactionHistoryRequest import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFactory import com.tangem.blockchainsdk.BlockchainSDKFactory @@ -769,6 +770,17 @@ internal class DefaultWalletManagersFacade @Inject constructor( return blockchain.getNFTExploreUrl(assetIdentifier) } + override suspend fun getTokenBalances(userWalletId: UserWalletId, network: Network): List { + val blockchain = network.toBlockchain() + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) ?: return emptyList() + val address = walletManager.wallet.address + return walletManager.getTokenBalances(address) + } + override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean { val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network) val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true diff --git a/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt index adf5c09070..3e193f5c8b 100644 --- a/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -12,6 +12,7 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.nft.models.NFTAsset import com.tangem.blockchain.nft.models.NFTCollection +import com.tangem.blockchain.tokenbalance.models.TokenBalance import com.tangem.blockchainsdk.models.UpdateWalletManagerResult import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -281,6 +282,8 @@ interface WalletManagersFacade { suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? + suspend fun getTokenBalances(userWalletId: UserWalletId, network: Network): List + /** * If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized] * value. Otherwise always return true diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 018768f225..552842c6e5 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -22,6 +22,7 @@ internal class PreviewDetailsComponent : DetailsComponent { router = DummyRouter(), hotWalletFeatureToggles = object : HotWalletFeatureToggles { override val isWalletCreationRestrictionEnabled: Boolean = true + override val isTokenSyncEnabled: Boolean = true }, ).buildAll( isWalletConnectAvailable = true, diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt index 551b32f7e5..84d2ec41e4 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt @@ -2,4 +2,5 @@ package com.tangem.features.hotwallet interface HotWalletFeatureToggles { val isWalletCreationRestrictionEnabled: Boolean + val isTokenSyncEnabled: Boolean } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt index 1eadc2342d..e3a007463d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt @@ -8,4 +8,7 @@ internal class DefaultHotWalletFeatureToggles( override val isWalletCreationRestrictionEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_CREATION_RESTRICTION_ENABLED") + + override val isTokenSyncEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "TOKEN_SYNC_ENABLED") } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index ee0ad7a24b..d5500335a1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -401,6 +401,7 @@ include(":data:balance-hiding") include(":data:common") include(":data:card") include(":data:tokens") +include(":data:tokensync") include(":data:settings") include(":data:txhistory") include(":data:wallets")