Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-03 15:39:22 +05:00
parent 2ce77a6ac3
commit cc07b0ba26
12 changed files with 129 additions and 95 deletions

View file

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

View file

@ -337,8 +337,7 @@ internal object TokensDomainModule {
@ViewModelScoped
fun provideRunPolkadotAccountHealthCheckUseCase(
repository: PolkadotAccountHealthCheckRepository,
dispatchers: CoroutineDispatcherProvider,
): RunPolkadotAccountHealthCheckUseCase {
return RunPolkadotAccountHealthCheckUseCase(repository, dispatchers)
return RunPolkadotAccountHealthCheckUseCase(repository)
}
}

View file

@ -72,6 +72,12 @@ class AppPreferencesStore(
return this[key]?.let(adapter::fromJson).orEmpty()
}
/** Get set of data [T] by string [key] */
inline fun <reified T> MutablePreferences.getObjectSet(key: Preferences.Key<String>): Set<T>? {
val adapter = moshi.adapter<Set<T>>(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 <reified T> MutablePreferences.setObjectSet(key: Preferences.Key<String>, value: Set<T>) {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
this[key] = adapter.toJson(value)
}
}

View file

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

View file

@ -128,6 +128,15 @@ suspend inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(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 <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
return data.firstOrNull()
?.get(key)
?.let(adapter::fromJson)

View file

@ -125,10 +125,12 @@ internal object TokensDataModule {
fun providePolkadotAccountHealthCheckRepository(
walletManagersFacade: WalletManagersFacade,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): PolkadotAccountHealthCheckRepository {
return DefaultPolkadotAccountHealthCheckRepository(
walletManagersFacade = walletManagersFacade,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}
}

View file

@ -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<Boolean>()
private val hasResetTransaction = MutableSharedFlow<Boolean>()
private val hasImmortalTransaction = MutableSharedFlow<Pair<String, Boolean>>()
private val hasResetTransaction = MutableSharedFlow<Pair<String, Boolean>>()
private val mutex = Mutex()
private val mutexes = ConcurrentHashMap<String, Mutex>()
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<String>(POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS)
val checkedAddresses = appPreferencesStore.getObjectSetSync<String>(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<String>(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS)
private suspend fun checkHasImmortal(accountCheckerProvider: AccountCheckProvider, address: String) {
val checkedAddresses =
appPreferencesStore.getObjectSetSync<String>(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY)
if (checkedAddresses.contains(address)) return
runCatching {
do {
// Getting batch of extrinsics to check
val lastChecked = appPreferencesStore.getObjectMap<Long>(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX)
val lastChecked = appPreferencesStore.getObjectMap<Long>(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<Long>(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX)
val savedList = it.getObjectMap<Long>(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<Long>(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX)
val savedList = mutablePreferences.getObjectMap<Long>(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<String>) {
appPreferencesStore.editData { mutablePreferences ->
val savedList = mutablePreferences.getObjectList<String>(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS)
val updatedList = savedList?.toMutableList()
updatedList?.addOrReplace(address) { it == address }
val savedList = mutablePreferences.getObjectSet<String>(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<String>(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)
}
}
}

View file

@ -6,5 +6,6 @@ import kotlinx.coroutines.flow.Flow
class GetPolkadotCheckHasImmortalUseCase(
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
) {
operator fun invoke(): Flow<Boolean> = polkadotAccountHealthCheckRepository.subscribeToHasImmortalResults()
operator fun invoke(): Flow<Pair<String, Boolean>> =
polkadotAccountHealthCheckRepository.subscribeToHasImmortalResults()
}

View file

@ -7,5 +7,6 @@ class GetPolkadotCheckHasResetUseCase(
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
) {
operator fun invoke(): Flow<Boolean> = polkadotAccountHealthCheckRepository.subscribeToHasResetResults()
operator fun invoke(): Flow<Pair<String, Boolean>> =
polkadotAccountHealthCheckRepository.subscribeToHasResetResults()
}

View file

@ -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<Throwable, Unit> =
withContext(dispatchers.io) {
Either.catch {
polkadotAccountHealthCheckRepository.runCheck(userWalletId, network)
}
}
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, Unit> = Either.catch {
polkadotAccountHealthCheckRepository.runCheck(userWalletId, network)
}
}

View file

@ -8,7 +8,7 @@ interface PolkadotAccountHealthCheckRepository {
suspend fun runCheck(userWalletId: UserWalletId, network: Network)
fun subscribeToHasImmortalResults(): Flow<Boolean>
fun subscribeToHasImmortalResults(): Flow<Pair<String, Boolean>>
fun subscribeToHasResetResults(): Flow<Boolean>
fun subscribeToHasResetResults(): Flow<Pair<String, Boolean>>
}

View file

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