Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ABW-3847] Integrate sargon logic for security problems in Android wallet #1274

Merged
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@ package com.babylon.wallet.android.domain.model
// https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3392569357/Security-related+Problem+States+in+the+Wallet
sealed interface SecurityProblem {

// security problem 3
data class EntitiesNotRecoverable(
val accountsNeedBackup: Int,
val personasNeedBackup: Int,
val hiddenAccountsNeedBackup: Int,
val hiddenPersonasNeedBackup: Int
) : SecurityProblem

data class SeedPhraseNeedRecovery(val isAnyActivePersonaAffected: Boolean) : SecurityProblem

sealed interface CloudBackupNotWorking : SecurityProblem {
// security problem 5
data class ServiceError(val isAnyActivePersonaAffected: Boolean) : CloudBackupNotWorking

// security problem 6 & 7
data class Disabled(
val isAnyActivePersonaAffected: Boolean,
val hasManualBackup: Boolean
) : CloudBackupNotWorking
}

// security problem 9
data class SeedPhraseNeedRecovery(val isAnyActivePersonaAffected: Boolean) : SecurityProblem

val hasCloudBackupProblems: Boolean
get() = when (this) {
is CloudBackupNotWorking.Disabled -> true
Expand All @@ -36,3 +41,41 @@ sealed interface SecurityProblem {
is CloudBackupNotWorking -> false
}
}

fun com.radixdlt.sargon.SecurityProblem.toDomainModel(
isAnyActivePersonaAffected: Boolean,
hasManualBackup: Boolean
): SecurityProblem {
return when (this) {
is com.radixdlt.sargon.SecurityProblem.Problem3 -> {
SecurityProblem.EntitiesNotRecoverable(
accountsNeedBackup = this.addresses.accounts.count(),
personasNeedBackup = this.addresses.personas.count(),
hiddenAccountsNeedBackup = this.addresses.hiddenAccounts.count(),
hiddenPersonasNeedBackup = this.addresses.hiddenPersonas.count()
)
}
com.radixdlt.sargon.SecurityProblem.Problem5 -> {
SecurityProblem.CloudBackupNotWorking.ServiceError(
isAnyActivePersonaAffected = isAnyActivePersonaAffected
)
}
com.radixdlt.sargon.SecurityProblem.Problem6 -> {
SecurityProblem.CloudBackupNotWorking.Disabled(
isAnyActivePersonaAffected = isAnyActivePersonaAffected,
hasManualBackup = hasManualBackup
)
}
com.radixdlt.sargon.SecurityProblem.Problem7 -> {
SecurityProblem.CloudBackupNotWorking.Disabled(
isAnyActivePersonaAffected = isAnyActivePersonaAffected,
hasManualBackup = hasManualBackup
)
}
is com.radixdlt.sargon.SecurityProblem.Problem9 -> {
SecurityProblem.SeedPhraseNeedRecovery(
isAnyActivePersonaAffected = this.addresses.personas.isNotEmpty()
)
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.babylon.wallet.android.domain.usecases
package com.babylon.wallet.android.domain.usecases.securityproblems

import com.radixdlt.sargon.AddressOfAccountOrPersona
import com.radixdlt.sargon.FactorSource
Expand Down Expand Up @@ -96,9 +96,9 @@ fun List<EntityWithSecurityPrompt>.personaPrompts() = mapNotNull {
}.associate { it }

enum class SecurityPromptType {
WRITE_DOWN_SEED_PHRASE,
RECOVERY_REQUIRED,
CONFIGURATION_BACKUP_PROBLEM,
WALLET_NOT_RECOVERABLE,
CONFIGURATION_BACKUP_NOT_UPDATED
WRITE_DOWN_SEED_PHRASE, // security problem 3
RECOVERY_REQUIRED, // security problem 9
CONFIGURATION_BACKUP_PROBLEM, // security problem 5
WALLET_NOT_RECOVERABLE, // security problem 6
CONFIGURATION_BACKUP_NOT_UPDATED // security problem 7
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.babylon.wallet.android.domain.usecases.securityproblems

import com.babylon.wallet.android.domain.model.SecurityProblem
import com.babylon.wallet.android.domain.model.toDomainModel
import com.radixdlt.sargon.AddressesOfEntitiesInBadState
import com.radixdlt.sargon.BackupResult
import com.radixdlt.sargon.CheckSecurityProblemsInput
import com.radixdlt.sargon.extensions.ProfileEntity
import com.radixdlt.sargon.os.SargonOsManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import rdx.works.core.domain.cloudbackup.BackupState
import rdx.works.core.sargon.factorSourceId
import rdx.works.core.sargon.isNotHidden
import rdx.works.profile.domain.backup.GetBackupStateUseCase
import javax.inject.Inject

class GetSecurityProblemsUseCase @Inject constructor(
private val getEntitiesWithSecurityPromptUseCase: GetEntitiesWithSecurityPromptUseCase,
private val getBackupStateUseCase: GetBackupStateUseCase,
private val sargonOsManager: SargonOsManager
) {

operator fun invoke(): Flow<Set<SecurityProblem>> = combine(
getEntitiesWithSecurityPromptUseCase(),
getBackupStateUseCase()
) { entitiesWithSecurityPrompts, backupState ->

// active personas that need cloud backup
val activePersonasNeedCloudBackup = entitiesWithSecurityPrompts
.count { entityWithSecurityPrompt ->
entityWithSecurityPrompt.entity.isNotHidden() &&
entityWithSecurityPrompt.entity is ProfileEntity.PersonaEntity &&
(
entityWithSecurityPrompt.prompts.contains(SecurityPromptType.CONFIGURATION_BACKUP_PROBLEM) ||
entityWithSecurityPrompt.prompts.contains(SecurityPromptType.WALLET_NOT_RECOVERABLE) ||
entityWithSecurityPrompt.prompts.contains(SecurityPromptType.CONFIGURATION_BACKUP_NOT_UPDATED)
)
}

sargonOsManager.sargonOs.checkSecurityProblems(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both sargonOs getter and checkSecurityProblems method on it are throwing. Please ensure the exception is properly handled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes good catch! thank you

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sergiupuhalschi-rdx what do you recommend about handling the exception?
adding a runCatching in the usecase and return Flow<Result<Set<SecurityProblem>>>
or adding a .catch { Timber.e("failed to get security problems...") } in the viewmodel where I collect the flows?

I’m leaning toward the second option, tbh.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we should just propagate the error with no additional logic then catch looks like a better option. You might also consider adding catch inside the useCase instead of each viewModel using this useCase. Either option sounds good.

input = CheckSecurityProblemsInput(
isCloudProfileSyncEnabled = backupState.isCloudBackupEnabled,
unrecoverableEntities = findUnrecoverableEntities(entitiesWithSecurityPrompts),
withoutControlEntities = findWithoutControlEntities(entitiesWithSecurityPrompts),
lastCloudBackup = BackupResult(
saveIdentifier = "string",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a dummy text because the field is only used by iOS? Maybe we'd benefit from having a comment here stating this if this is the case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot to update this.
I will pass the google drive file id there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isCurrent = backupState.isCloudBackupSynced,
isFailed = backupState is BackupState.CloudBackupEnabled && backupState.hasAnyErrors
),
lastManualBackup = BackupResult(
saveIdentifier = "string",
isCurrent = backupState.isManualBackupSynced,
isFailed = false,
)
)
).map { sargonSecurityProblem ->
sargonSecurityProblem.toDomainModel(
isAnyActivePersonaAffected = activePersonasNeedCloudBackup > 0,
hasManualBackup = backupState.lastManualBackupTime != null
)
}.toSet()
}

private fun findUnrecoverableEntities(entitiesWithSecurityPrompts: List<EntityWithSecurityPrompt>): AddressesOfEntitiesInBadState {
// entities that need to write down seed phrase
val entitiesNeedBackup = entitiesWithSecurityPrompts
.filter { entityWithSecurityPrompt ->
entityWithSecurityPrompt.prompts.contains(SecurityPromptType.WRITE_DOWN_SEED_PHRASE)
}
val factorSourceIdsNeedBackup = entitiesNeedBackup.map { entityWithSecurityPrompt ->
entityWithSecurityPrompt.entity.securityState.factorSourceId
}.toSet()
// not hidden and hidden accounts that need to write down seed phrase
val (activeAccountAddressesNeedBackup, hiddenAccountAddressesNeedBackup) = entitiesNeedBackup
.filter { entityWithSecurityPrompt ->
entityWithSecurityPrompt.entity is ProfileEntity.AccountEntity &&
factorSourceIdsNeedBackup.contains(entityWithSecurityPrompt.entity.securityState.factorSourceId)
}
.map { entityWithSecurityPrompt -> entityWithSecurityPrompt.entity as ProfileEntity.AccountEntity }
.partition { accountEntity -> accountEntity.isNotHidden() }
.let { partitioned ->
partitioned.first.map { it.accountAddress } to partitioned.second.map { it.accountAddress }
}
// not hidden and hidden personas that need to write down seed phrase
val (activePersonaAddressesNeedBackup, hiddenPersonaAddressesNeedBackup) = entitiesNeedBackup
.filter { entityWithSecurityPrompt ->
entityWithSecurityPrompt.entity is ProfileEntity.PersonaEntity &&
factorSourceIdsNeedBackup.contains(entityWithSecurityPrompt.entity.securityState.factorSourceId)
}
.map { entityWithSecurityPrompt -> entityWithSecurityPrompt.entity as ProfileEntity.PersonaEntity }
.partition { personaEntity -> personaEntity.isNotHidden() }
.let { partitioned ->
partitioned.first.map { it.identityAddress } to partitioned.second.map { it.identityAddress }
}

return AddressesOfEntitiesInBadState(
accounts = activeAccountAddressesNeedBackup,
hiddenAccounts = hiddenAccountAddressesNeedBackup,
personas = activePersonaAddressesNeedBackup,
hiddenPersonas = hiddenPersonaAddressesNeedBackup
)
}

private fun findWithoutControlEntities(entitiesWithSecurityPrompts: List<EntityWithSecurityPrompt>): AddressesOfEntitiesInBadState {
// entities that need recovery
val entitiesNeedRecovery =
entitiesWithSecurityPrompts.filter { it.prompts.contains(SecurityPromptType.RECOVERY_REQUIRED) }
val (activeEntitiesNeedRecovery, hiddenEntitiesNeedRecovery) = entitiesNeedRecovery
.partition { it.entity.isNotHidden() }
val activeAccountAddressesNeedRecovery = activeEntitiesNeedRecovery
.mapNotNull { (it.entity as? ProfileEntity.AccountEntity)?.accountAddress }
val activePersonaAddressesNeedRecovery = activeEntitiesNeedRecovery
.mapNotNull { (it.entity as? ProfileEntity.PersonaEntity)?.identityAddress }
val hiddenAccountAddressesNeedRecovery = hiddenEntitiesNeedRecovery
.mapNotNull { (it.entity as? ProfileEntity.AccountEntity)?.accountAddress }
val hiddenPersonaAddressesNeedRecovery = hiddenEntitiesNeedRecovery
.mapNotNull { (it.entity as? ProfileEntity.PersonaEntity)?.identityAddress }

return AddressesOfEntitiesInBadState(
accounts = activeAccountAddressesNeedRecovery,
hiddenAccounts = hiddenAccountAddressesNeedRecovery,
personas = activePersonaAddressesNeedRecovery,
hiddenPersonas = hiddenPersonaAddressesNeedRecovery
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import com.babylon.wallet.android.designsystem.theme.gradient
import com.babylon.wallet.android.designsystem.theme.plus
import com.babylon.wallet.android.domain.model.assets.AccountWithAssets
import com.babylon.wallet.android.domain.model.locker.AccountLockerDeposit
import com.babylon.wallet.android.domain.usecases.SecurityPromptType
import com.babylon.wallet.android.domain.usecases.securityproblems.SecurityPromptType
import com.babylon.wallet.android.presentation.account.AccountViewModel.Event
import com.babylon.wallet.android.presentation.account.AccountViewModel.State
import com.babylon.wallet.android.presentation.dialogs.info.GlossaryItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import androidx.constraintlayout.compose.MotionScene
import com.babylon.wallet.android.R
import com.babylon.wallet.android.designsystem.composable.RadixSecondaryButton
import com.babylon.wallet.android.designsystem.theme.RadixTheme
import com.babylon.wallet.android.domain.usecases.SecurityPromptType
import com.babylon.wallet.android.domain.usecases.securityproblems.SecurityPromptType
import com.babylon.wallet.android.presentation.account.AccountViewModel.State
import com.babylon.wallet.android.presentation.ui.composables.AccountPromptLabel
import com.babylon.wallet.android.presentation.ui.composables.actionableaddress.ActionableAddressView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import com.babylon.wallet.android.data.repository.tokenprice.FiatPriceRepository
import com.babylon.wallet.android.di.coroutines.DefaultDispatcher
import com.babylon.wallet.android.domain.model.assets.AccountWithAssets
import com.babylon.wallet.android.domain.model.locker.AccountLockerDeposit
import com.babylon.wallet.android.domain.usecases.GetEntitiesWithSecurityPromptUseCase
import com.babylon.wallet.android.domain.usecases.GetNetworkInfoUseCase
import com.babylon.wallet.android.domain.usecases.SecurityPromptType
import com.babylon.wallet.android.domain.usecases.accountPrompts
import com.babylon.wallet.android.domain.usecases.assets.GetFiatValueUseCase
import com.babylon.wallet.android.domain.usecases.assets.GetNextNFTsPageUseCase
import com.babylon.wallet.android.domain.usecases.assets.GetWalletAssetsUseCase
import com.babylon.wallet.android.domain.usecases.assets.UpdateLSUsInfo
import com.babylon.wallet.android.domain.usecases.securityproblems.GetEntitiesWithSecurityPromptUseCase
import com.babylon.wallet.android.domain.usecases.securityproblems.SecurityPromptType
import com.babylon.wallet.android.domain.usecases.securityproblems.accountPrompts
import com.babylon.wallet.android.domain.usecases.transaction.SendClaimRequestUseCase
import com.babylon.wallet.android.presentation.account.delegates.AccountLockersDelegate
import com.babylon.wallet.android.presentation.common.OneOffEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.babylon.wallet.android.BuildConfig.EXPERIMENTAL_FEATURES_ENABLED
import com.babylon.wallet.android.di.coroutines.DefaultDispatcher
import com.babylon.wallet.android.domain.model.SecurityProblem
import com.babylon.wallet.android.domain.usecases.GetSecurityProblemsUseCase
import com.babylon.wallet.android.domain.usecases.securityproblems.GetSecurityProblemsUseCase
import com.babylon.wallet.android.presentation.settings.SettingsItem.TopLevelSettings.DebugSettings
import com.babylon.wallet.android.presentation.settings.SettingsItem.TopLevelSettings.Personas
import com.babylon.wallet.android.presentation.settings.SettingsItem.TopLevelSettings.Preferences
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import com.babylon.wallet.android.R
import com.babylon.wallet.android.designsystem.composable.RadixSecondaryButton
import com.babylon.wallet.android.designsystem.theme.RadixTheme
import com.babylon.wallet.android.designsystem.theme.RadixWalletTheme
import com.babylon.wallet.android.domain.usecases.EntityWithSecurityPrompt
import com.babylon.wallet.android.domain.usecases.SecurityPromptType
import com.babylon.wallet.android.domain.usecases.securityproblems.EntityWithSecurityPrompt
import com.babylon.wallet.android.domain.usecases.securityproblems.SecurityPromptType
import com.babylon.wallet.android.presentation.dialogs.info.GlossaryItem
import com.babylon.wallet.android.presentation.settings.personas.PersonasViewModel.PersonasEvent
import com.babylon.wallet.android.presentation.ui.composables.InfoButton
Expand Down
Loading
Loading