Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-04 11:40:26 +01:00
commit 9352a2a899
22 changed files with 421 additions and 22 deletions

View file

@ -19,12 +19,14 @@ import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -35,10 +37,13 @@ import com.tangem.data.card.sdk.CardSdkLifecycleObserver
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.tester.api.TesterRouter
@ -99,6 +104,7 @@ val userWalletsListManagerSafe: UserWalletsListManager?
val userWalletsListManager: UserWalletsListManager
get() = userWalletsListManagerSafe!!
@Suppress("LargeClass")
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
@ -148,6 +154,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var generalUserWalletsListManager: UserWalletsListManager
@Inject
lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase
@Inject
lateinit var getPolkadotCheckHasImmortalUseCase: GetPolkadotCheckHasImmortalUseCase
@Inject
lateinit var analyticsEventsHandler: AnalyticsEventHandler
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
@ -177,6 +192,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
checkForNotificationPermission()
observeStateUpdates()
observePolkadotAccountHealthCheck()
if (intent != null) {
deepLinksRegistry.launch(intent)
@ -482,4 +498,25 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0)
}
}
private fun observePolkadotAccountHealthCheck() {
lifecycleScope.launch {
getPolkadotCheckHasResetUseCase()
.flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED)
.distinctUntilChanged()
.collect {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.Token.PolkadotAccountReset(it.second))
}
}
lifecycleScope.launch {
getPolkadotCheckHasImmortalUseCase()
.flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED)
.distinctUntilChanged()
.collect {
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.Token.PolkadotImmortalTransactions(it.second),
)
}
}
}
}

View file

@ -4,6 +4,9 @@ import android.content.Context
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager
@ -57,4 +60,20 @@ internal object ActivityModule {
fun provideActivityDelayedWorkCoroutineScope(): CoroutineScope {
return CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
@Provides
@Singleton
fun provideGetPolkadotCheckHasResetUseCase(
polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
): GetPolkadotCheckHasResetUseCase {
return GetPolkadotCheckHasResetUseCase(polkadotAccountHealthCheckRepository)
}
@Provides
@Singleton
fun provideGetPolkadotCheckHasImmortalUseCase(
polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
): GetPolkadotCheckHasImmortalUseCase {
return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository)
}
}

View file

@ -332,4 +332,12 @@ internal object TokensDomainModule {
): IsAmountSubtractAvailableUseCase {
return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideRunPolkadotAccountHealthCheckUseCase(
repository: PolkadotAccountHealthCheckRepository,
): RunPolkadotAccountHealthCheckUseCase {
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,6 +63,16 @@ object PreferencesKeys {
val APP_LOGS_KEY by lazy { stringPreferencesKey(name = "app_logs") }
val POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY by lazy {
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX")
}
val POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY by lazy {
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS")
}
val POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY by lazy {
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS")
}
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
}

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

@ -119,4 +119,18 @@ internal object TokensDataModule {
fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository {
return DefaultCurrencyChecksRepository(walletManagersFacade = walletManagersFacade)
}
@Provides
@Singleton
fun providePolkadotAccountHealthCheckRepository(
walletManagersFacade: WalletManagersFacade,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): PolkadotAccountHealthCheckRepository {
return DefaultPolkadotAccountHealthCheckRepository(
walletManagersFacade = walletManagersFacade,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}
}

View file

@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import timber.log.Timber
@Suppress("LongParameterList")
internal class DefaultNetworksRepository(
private val networksStatusesStore: NetworksStatusesStore,
private val walletManagersFacade: WalletManagersFacade,

View file

@ -0,0 +1,179 @@
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_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.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<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
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
}
// 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)
}
}
}
override fun subscribeToHasImmortalResults() = hasImmortalTransaction.asSharedFlow()
override fun subscribeToHasResetResults() = hasResetTransaction.asSharedFlow()
private suspend fun checkHasReset(polkadotManager: AccountCheckProvider, address: String) {
val checkedAddresses = appPreferencesStore.getObjectSetSync<String>(POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY)
if (checkedAddresses.contains(address)) return
runCatching {
val accountInfo = requireNotNull(polkadotManager.getAccountInfo().account) {
Timber.e("Account info is null")
}
val nonce = accountInfo.nonce
val extrinsicCount = accountInfo.countExtrinsic
// Account was reset
if (nonce != null && extrinsicCount != null) {
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) {
updateChecked(address, POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY)
}
}
}
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_KEY)
val lastExtrinsic = lastChecked[address]
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(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_KEY)
val updatedList = savedList.toMutableMap()
updatedList[address] = txId
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_KEY)
val updatedList = savedList.toMutableMap()
updatedList.remove(address)
mutablePreferences.setObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY, updatedList)
}
}
private suspend fun updateChecked(address: String, key: Preferences.Key<String>) {
appPreferencesStore.editData { mutablePreferences ->
val savedList = mutablePreferences.getObjectSet<String>(key)
val updatedList = savedList?.toMutableSet() ?: mutableSetOf()
updatedList.add(address)
if (updatedList.isNotEmpty()) {
mutablePreferences.setObjectSet(
key,
updatedList.toSet(),
)
} else {
mutablePreferences.remove(key)
}
}
}
private companion object {
const val ACCOUNT_NOT_FOUND = "Record Not Found"
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.tokens
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import kotlinx.coroutines.flow.Flow
class GetPolkadotCheckHasImmortalUseCase(
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
) {
operator fun invoke(): Flow<Pair<String, Boolean>> =
polkadotAccountHealthCheckRepository.subscribeToHasImmortalResults()
}

View file

@ -0,0 +1,12 @@
package com.tangem.domain.tokens
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import kotlinx.coroutines.flow.Flow
class GetPolkadotCheckHasResetUseCase(
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
) {
operator fun invoke(): Flow<Pair<String, Boolean>> =
polkadotAccountHealthCheckRepository.subscribeToHasResetResults()
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.tokens
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
class RunPolkadotAccountHealthCheckUseCase(
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, Unit> = Either.catch {
polkadotAccountHealthCheckRepository.runCheck(userWalletId, network)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface PolkadotAccountHealthCheckRepository {
suspend fun runCheck(userWalletId: UserWalletId, network: Network)
fun subscribeToHasImmortalResults(): Flow<Pair<String, Boolean>>
fun subscribeToHasResetResults(): Flow<Pair<String, Boolean>>
}

View file

@ -23,7 +23,7 @@ sealed class WalletScreenAnalyticsEvent {
override val oneTimeEventId: String = id + userWalletId.stringValue
}
object WalletOpened : Basic(event = "Wallet Opened")
data object WalletOpened : Basic(event = "Wallet Opened")
class CardWasScanned(source: AnalyticsParam.ScreensSources) : Basic(
event = "Card Was Scanned",
@ -48,13 +48,33 @@ sealed class WalletScreenAnalyticsEvent {
)
}
sealed class Token(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Token", event = event, params = params) {
class PolkadotAccountReset(hasReset: Boolean) : Token(
event = "Polkadot Account Reset",
params = mapOf(
AnalyticsParam.STATE to if (hasReset) "Yes" else "No",
),
)
class PolkadotImmortalTransactions(hasImmortalTransaction: Boolean) : Token(
event = "Polkadot Immortal Transactions",
params = mapOf(
AnalyticsParam.STATE to if (hasImmortalTransaction) "Yes" else "No",
),
)
}
sealed class MainScreen(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
object ScreenOpened : MainScreen(event = "Screen opened")
object WalletSwipe : MainScreen(event = "Wallet Swipe")
data object ScreenOpened : MainScreen(event = "Screen opened")
data object WalletSwipe : MainScreen(event = "Wallet Swipe")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
event = "Enable Biometric",
@ -66,37 +86,37 @@ sealed class WalletScreenAnalyticsEvent {
params = mapOf("Result" to result.value),
)
object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked")
object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
data object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked")
data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable")
data object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable")
object MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
data object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
data object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
data object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
object TestnetCard : MainScreen(event = "Notice - Testnet Card")
data object TestnetCard : MainScreen(event = "Notice - Testnet Card")
object DemoCard : MainScreen(event = "Notice - Demo Card")
data object DemoCard : MainScreen(event = "Notice - Demo Card")
object DevelopmentCard : MainScreen(event = "Notice - Development Card")
data object DevelopmentCard : MainScreen(event = "Notice - Development Card")
object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
data object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
data object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -29,6 +30,7 @@ internal class MultiWalletContentLoader(
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val reduxStateHolder: ReduxStateHolder,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> {
@ -42,6 +44,7 @@ internal class MultiWalletContentLoader(
getTokenListUseCase = getTokenListUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
),
MultiWalletWarningsSubscriber(
userWallet = userWallet,

View file

@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -26,6 +27,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader {
@ -41,6 +43,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
reduxStateHolder = reduxStateHolder,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -24,6 +25,7 @@ internal class SingleWalletWithTokenContentLoader(
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val getCardTokensListUseCase: GetCardTokensListUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
override fun create(): List<WalletSubscriber> {
@ -36,6 +38,7 @@ internal class SingleWalletWithTokenContentLoader(
walletWithFundsChecker = walletWithFundsChecker,
getCardTokensListUseCase = getCardTokensListUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
),
MultiWalletWarningsSubscriber(
userWallet = userWallet,

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
@ -21,6 +22,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
private val getCardTokensListUseCase: GetCardTokensListUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader {
@ -34,6 +36,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
getCardTokensListUseCase = getCardTokensListUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
)
}
}

View file

@ -4,7 +4,9 @@ import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
@ -33,6 +35,7 @@ internal abstract class BasicTokenListSubscriber(
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : WalletSubscriber() {
private val sendAnalyticsJobHolder = JobHolder()
@ -53,6 +56,8 @@ internal abstract class BasicTokenListSubscriber(
coroutineScope.launch {
onTokenListReceived(maybeTokenList)
}.saveIn(onTokenListReceivedJobHolder)
coroutineScope.launch { startCheck(maybeTokenList) }
},
flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
transform = { maybeTokenList, maybeAppCurrency ->
@ -72,6 +77,21 @@ internal abstract class BasicTokenListSubscriber(
)
}
private suspend fun startCheck(maybeTokenList: Either<TokenListError, TokenList>) {
// Run Polkadot account health check
maybeTokenList.getOrNull()?.let { tokenList ->
val cryptoCurrencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
cryptoCurrencies.forEach {
runPolkadotAccountHealthCheckUseCase(userWallet.walletId, it.currency.network)
}
}
}
protected open suspend fun onTokenListReceived(maybeTokenList: Either<TokenListError, TokenList>) {
/* no-op */
}

View file

@ -5,6 +5,7 @@ import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.TokenList
@ -24,6 +25,7 @@ internal class MultiWalletTokenListSubscriber(
tokenListAnalyticsSender: TokenListAnalyticsSender,
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
@ -31,6 +33,7 @@ internal class MultiWalletTokenListSubscriber(
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override fun tokenListFlow(): MaybeTokenListFlow = getTokenListUseCase(userWallet.walletId)

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
@ -17,6 +18,7 @@ internal class SingleWalletWithTokenListSubscriber(
tokenListAnalyticsSender: TokenListAnalyticsSender,
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
@ -24,6 +26,7 @@ internal class SingleWalletWithTokenListSubscriber(
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override fun tokenListFlow(): MaybeTokenListFlow = getCardTokensListUseCase(userWallet.walletId)

View file

@ -85,7 +85,7 @@ web3j = "4.10.1"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "release-app_5.8-536"
tangemBlockchainSdk = "release-app_5.8-562"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.8-341"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^