diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 3a06ab01ef..2a453f6cc8 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -503,13 +503,19 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lifecycleScope.launch { getPolkadotCheckHasResetUseCase() .flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED) + .distinctUntilChanged() .collect { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.Token.PolkadotAccountReset(it)) + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.Token.PolkadotAccountReset(it.second)) } + } + lifecycleScope.launch { getPolkadotCheckHasImmortalUseCase() .flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED) + .distinctUntilChanged() .collect { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.Token.PolkadotImmortalTransactions(it)) + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.Token.PolkadotImmortalTransactions(it.second), + ) } } } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 8f377b67ad..2e373e7c69 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -337,8 +337,7 @@ internal object TokensDomainModule { @ViewModelScoped fun provideRunPolkadotAccountHealthCheckUseCase( repository: PolkadotAccountHealthCheckRepository, - dispatchers: CoroutineDispatcherProvider, ): RunPolkadotAccountHealthCheckUseCase { - return RunPolkadotAccountHealthCheckUseCase(repository, dispatchers) + return RunPolkadotAccountHealthCheckUseCase(repository) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt index 2b1de6b7b0..905fe4f767 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt @@ -72,6 +72,12 @@ class AppPreferencesStore( return this[key]?.let(adapter::fromJson).orEmpty() } + /** Get set of data [T] by string [key] */ + inline fun MutablePreferences.getObjectSet(key: Preferences.Key): Set? { + val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) + return this[key]?.let(adapter::fromJson) + } + /** * Set data [T] by string [key] to [MutablePreferences] * @@ -97,4 +103,10 @@ class AppPreferencesStore( this[key] = adapter.toJson(value) } + + /** Sets set of data [T] by string [key] to [MutablePreferences] */ + inline fun MutablePreferences.setObjectSet(key: Preferences.Key, value: Set) { + val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) + this[key] = adapter.toJson(value) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 464387831e..2a6b0457d3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -63,13 +63,13 @@ object PreferencesKeys { val APP_LOGS_KEY by lazy { stringPreferencesKey(name = "app_logs") } - val POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX by lazy { + val POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY by lazy { stringPreferencesKey(name = "POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX") } - val POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS by lazy { + val POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY by lazy { stringPreferencesKey(name = "POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS") } - val POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS by lazy { + val POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY by lazy { stringPreferencesKey(name = "POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS") } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index cd1facee55..aa3171803c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -128,6 +128,15 @@ suspend inline fun AppPreferencesStore.getObjectMap(key: Preferences val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) val adapter = moshi.adapter>(type) + return data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) + .orEmpty() +} + +/** Get set of data [T] by string [key], or empty if data is not found */ +suspend inline fun AppPreferencesStore.getObjectSetSync(key: Preferences.Key): Set { + val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) return data.firstOrNull() ?.get(key) ?.let(adapter::fromJson) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index ef0aef1c7e..062692cd06 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -125,10 +125,12 @@ internal object TokensDataModule { fun providePolkadotAccountHealthCheckRepository( walletManagersFacade: WalletManagersFacade, appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, ): PolkadotAccountHealthCheckRepository { return DefaultPolkadotAccountHealthCheckRepository( walletManagersFacade = walletManagersFacade, appPreferencesStore = appPreferencesStore, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt index e43b946f97..049991280b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt @@ -1,49 +1,64 @@ package com.tangem.data.tokens.repository +import androidx.datastore.preferences.core.Preferences import com.tangem.blockchain.blockchains.polkadot.AccountCheckProvider +import com.tangem.blockchain.blockchains.polkadot.network.accounthealthcheck.ExtrinsicListItemResponse import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS -import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS -import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX -import com.tangem.datasource.local.preferences.utils.getObjectListSync +import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY import com.tangem.datasource.local.preferences.utils.getObjectMap +import com.tangem.datasource.local.preferences.utils.getObjectSetSync import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.extensions.addOrReplace +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import kotlin.collections.set internal class DefaultPolkadotAccountHealthCheckRepository( private val walletManagersFacade: WalletManagersFacade, private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, ) : PolkadotAccountHealthCheckRepository { - private val hasImmortalTransaction = MutableSharedFlow() - private val hasResetTransaction = MutableSharedFlow() + private val hasImmortalTransaction = MutableSharedFlow>() + private val hasResetTransaction = MutableSharedFlow>() private val mutex = Mutex() + private val mutexes = ConcurrentHashMap() override suspend fun runCheck(userWalletId: UserWalletId, network: Network) { // Run Polkadot account health check if (Blockchain.fromId(network.id.value) != Blockchain.Polkadot) return - mutex.withLock { - val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) - val accountCheckProvider = requireNotNull(walletManager as AccountCheckProvider) { - Timber.e("Unable to cast wallet manager to AccountCheckProvider") - return - } - val address = walletManager.wallet.address + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) + val address = requireNotNull(walletManager?.wallet?.address) { + Timber.e("Address is null") + return + } + val accountCheckProvider = requireNotNull(walletManager as? AccountCheckProvider) { + Timber.e("Unable to cast wallet manager to AccountCheckProvider") + return + } - checkHasReset(accountCheckProvider, address) - checkHasImmortal(accountCheckProvider, address) + // use a separate mutexForKey for each key to avoid multiple calls block() to the same key + // also used mutex to safe create mutexForKey, otherwise it can lead to multiple calls for the same key + val mutexForKey = mutex.withLock { mutexes.getOrPut(address) { Mutex() } } + mutexForKey.withLock { + withContext(dispatchers.io) { + checkHasReset(accountCheckProvider, address) + checkHasImmortal(accountCheckProvider, address) + } } } @@ -52,7 +67,7 @@ internal class DefaultPolkadotAccountHealthCheckRepository( override fun subscribeToHasResetResults() = hasResetTransaction.asSharedFlow() private suspend fun checkHasReset(polkadotManager: AccountCheckProvider, address: String) { - val checkedAddresses = appPreferencesStore.getObjectListSync(POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS) + val checkedAddresses = appPreferencesStore.getObjectSetSync(POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY) if (checkedAddresses.contains(address)) return runCatching { val accountInfo = requireNotNull(polkadotManager.getAccountInfo().account) { @@ -63,102 +78,97 @@ internal class DefaultPolkadotAccountHealthCheckRepository( // Account was reset if (nonce != null && extrinsicCount != null) { - hasResetTransaction.emit(nonce < extrinsicCount) - updateCheckedResetAddressesList(address) + val hasReset = nonce < extrinsicCount + updateChecked(address, POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY) + hasResetTransaction.emit(address to hasReset) } }.onFailure { if ((it as? BlockchainSdkError.CustomError)?.customMessage == ACCOUNT_NOT_FOUND) { - updateCheckedResetAddressesList(address) + updateChecked(address, POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY) } } } - private suspend fun checkHasImmortal(polkadotManager: AccountCheckProvider, address: String) { - val checkedAddresses = appPreferencesStore.getObjectListSync(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS) + private suspend fun checkHasImmortal(accountCheckerProvider: AccountCheckProvider, address: String) { + val checkedAddresses = + appPreferencesStore.getObjectSetSync(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY) if (checkedAddresses.contains(address)) return runCatching { do { // Getting batch of extrinsics to check - val lastChecked = appPreferencesStore.getObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX) + val lastChecked = appPreferencesStore.getObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY) val lastExtrinsic = lastChecked[address] - val extrinsicListResult = polkadotManager.getExtrinsicList(afterExtrinsicId = lastExtrinsic) - // Checking extrinsic one by one - extrinsicListResult.extrinsic?.forEach { tx -> - val hash = tx.hash - val id = tx.id - if (hash != null && id != null) { - val details = polkadotManager.getExtrinsicDetail(hash) - - // We found an `immortal` transaction - if (details.lifetime == null) { - hasImmortalTransaction.emit(true) - updateCheckedImmutableAddressesList(address) - clearLastCheckedTransaction(address) - return - } - - // Saving last checked transaction - updateLastCheckedTransaction(address, id) + val extrinsicListResult = accountCheckerProvider.getExtrinsicList(afterExtrinsicId = lastExtrinsic) + extrinsicListResult.extrinsic + ?.forEach { tx -> + // Checking extrinsic one by one + if (checkTx(tx, address, accountCheckerProvider)) return } - } } while (!extrinsicListResult.extrinsic.isNullOrEmpty()) // We checked all transactions up to current moment and did not found an `immortal` transaction - hasImmortalTransaction.emit(false) - updateCheckedImmutableAddressesList(address) + hasImmortalTransaction.emit(address to false) + updateChecked(address, POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY) clearLastCheckedTransaction(address) } } + private suspend fun checkTx( + tx: ExtrinsicListItemResponse, + address: String, + accountCheckerProvider: AccountCheckProvider, + ): Boolean { + val hash = tx.hash + val id = tx.id + if (hash != null && id != null) { + val details = accountCheckerProvider.getExtrinsicDetail(hash) + + // We found an `immortal` transaction + if (details.lifetime == null) { + hasImmortalTransaction.emit(address to true) + updateChecked(address, POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY) + clearLastCheckedTransaction(address) + return true + } + + // Saving last checked transaction + updateLastCheckedTransaction(address, id) + } + return false + } + private suspend fun updateLastCheckedTransaction(address: String, txId: Long) { appPreferencesStore.editData { - val savedList = it.getObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX) + val savedList = it.getObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY) val updatedList = savedList.toMutableMap() updatedList[address] = txId - it.setObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX, updatedList) + it.setObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY, updatedList) } } private suspend fun clearLastCheckedTransaction(address: String) { appPreferencesStore.editData { mutablePreferences -> - val savedList = mutablePreferences.getObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX) + val savedList = mutablePreferences.getObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY) val updatedList = savedList.toMutableMap() updatedList.remove(address) - mutablePreferences.setObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX, updatedList) + mutablePreferences.setObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY, updatedList) } } - private suspend fun updateCheckedImmutableAddressesList(address: String) { + private suspend fun updateChecked(address: String, key: Preferences.Key) { appPreferencesStore.editData { mutablePreferences -> - val savedList = mutablePreferences.getObjectList(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS) - val updatedList = savedList?.toMutableList() - updatedList?.addOrReplace(address) { it == address } + val savedList = mutablePreferences.getObjectSet(key) + val updatedList = savedList?.toMutableSet() ?: mutableSetOf() + updatedList.add(address) - if (updatedList != null) { - mutablePreferences.setObjectList( - POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS, - updatedList.toList(), + if (updatedList.isNotEmpty()) { + mutablePreferences.setObjectSet( + key, + updatedList.toSet(), ) } else { - mutablePreferences.remove(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS) - } - } - } - - private suspend fun updateCheckedResetAddressesList(address: String) { - appPreferencesStore.editData { mutablePreferences -> - val savedList = mutablePreferences.getObjectList(POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS) - val updatedList = savedList?.toMutableList() - updatedList?.addOrReplace(address) { it == address } - - if (updatedList != null) { - mutablePreferences.setObjectList( - POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS, - updatedList.toList(), - ) - } else { - mutablePreferences.remove(POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS) + mutablePreferences.remove(key) } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasImmortalUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasImmortalUseCase.kt index 04df6436d3..527f6e1703 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasImmortalUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasImmortalUseCase.kt @@ -6,5 +6,6 @@ import kotlinx.coroutines.flow.Flow class GetPolkadotCheckHasImmortalUseCase( private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository, ) { - operator fun invoke(): Flow = polkadotAccountHealthCheckRepository.subscribeToHasImmortalResults() + operator fun invoke(): Flow> = + polkadotAccountHealthCheckRepository.subscribeToHasImmortalResults() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasResetUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasResetUseCase.kt index b67e44d13b..33ce7e960e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasResetUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasResetUseCase.kt @@ -7,5 +7,6 @@ class GetPolkadotCheckHasResetUseCase( private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository, ) { - operator fun invoke(): Flow = polkadotAccountHealthCheckRepository.subscribeToHasResetResults() + operator fun invoke(): Flow> = + polkadotAccountHealthCheckRepository.subscribeToHasResetResults() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RunPolkadotAccountHealthCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RunPolkadotAccountHealthCheckUseCase.kt index 8afa023e85..f7b3131361 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RunPolkadotAccountHealthCheckUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RunPolkadotAccountHealthCheckUseCase.kt @@ -4,18 +4,12 @@ import arrow.core.Either import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext class RunPolkadotAccountHealthCheckUseCase( private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository, - private val dispatchers: CoroutineDispatcherProvider, ) { - suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either = - withContext(dispatchers.io) { - Either.catch { - polkadotAccountHealthCheckRepository.runCheck(userWalletId, network) - } - } + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either = Either.catch { + polkadotAccountHealthCheckRepository.runCheck(userWalletId, network) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PolkadotAccountHealthCheckRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PolkadotAccountHealthCheckRepository.kt index 2da0aa6226..a0cd15dd47 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PolkadotAccountHealthCheckRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PolkadotAccountHealthCheckRepository.kt @@ -8,7 +8,7 @@ interface PolkadotAccountHealthCheckRepository { suspend fun runCheck(userWalletId: UserWalletId, network: Network) - fun subscribeToHasImmortalResults(): Flow + fun subscribeToHasImmortalResults(): Flow> - fun subscribeToHasResetResults(): Flow + fun subscribeToHasResetResults(): Flow> } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 838452da7b..6430f26a99 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ web3j = "4.10.1" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.8-542" +tangemBlockchainSdk = "release-app_5.8-562" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.8-338" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^