[#483] Fix crash when wiping wallet

This commit is contained in:
Carter Jernigan 2022-06-13 12:47:22 -04:00 committed by GitHub
parent 981d70727b
commit 3d5ed7b10b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 330 additions and 147 deletions

View File

@ -13,6 +13,7 @@ import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
@ -66,6 +67,7 @@ class SettingsViewTest : UiTestPrerequisites() {
@Test
@MediumTest
@Ignore("Wipe has been disabled in Settings and is now a debug-only option")
fun wipe() = runTest {
val testSetup = TestSetup(composeTestRule)

View File

@ -5,14 +5,18 @@ import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.ext.onFirst
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.sdk.model.PersistableWallet
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.spackle.LazyWithArgument
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@ -20,17 +24,26 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.util.UUID
class WalletCoordinator(context: Context) {
companion object {
@ -61,29 +74,57 @@ class WalletCoordinator(context: Context) {
emitAll(EncryptedPreferenceKeys.PERSISTABLE_WALLET.observe(encryptedPreferenceProvider))
}
private val lockoutMutex = Mutex()
private val synchronizerLockoutId = MutableStateFlow<UUID?>(null)
private sealed class InternalSynchronizerStatus {
object NoWallet : InternalSynchronizerStatus()
class Available(val synchronizer: cash.z.ecc.android.sdk.Synchronizer) : InternalSynchronizerStatus()
class Lockout(val id: UUID) : InternalSynchronizerStatus()
}
private val synchronizerOrLockoutId: Flow<Flow<InternalSynchronizerStatus>> = persistableWallet
.combine(synchronizerLockoutId) { persistableWallet: PersistableWallet?, lockoutId: UUID? ->
if (null != lockoutId) { // this one needs to come first
flowOf(InternalSynchronizerStatus.Lockout(lockoutId))
} else if (null == persistableWallet) {
flowOf(InternalSynchronizerStatus.NoWallet)
} else {
callbackFlow<InternalSynchronizerStatus.Available> {
val initializer = Initializer.new(context, persistableWallet.toConfig())
val synchronizer = synchronizerMutex.withLock {
val synchronizer = Synchronizer.new(initializer)
synchronizer.start(walletScope)
}
trySend(InternalSynchronizerStatus.Available(synchronizer))
awaitClose {
Twig.info { "Closing flow and stopping synchronizer" }
synchronizer.stop()
}
}
}
}
/**
* Synchronizer for the Zcash SDK. Emits null until a wallet secret is persisted.
*
* Note that this synchronizer is closed as soon as it stops being collected. For UI use
* cases, see [WalletViewModel].
*/
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
val synchronizer: StateFlow<Synchronizer?> = persistableWallet
.filterNotNull()
.flatMapConcat {
callbackFlow {
val initializer = Initializer.new(context, it.toConfig())
val synchronizer = synchronizerMutex.withLock {
val synchronizer = Synchronizer.new(initializer)
synchronizer.start(walletScope)
}
trySend(synchronizer)
awaitClose {
synchronizer.stop()
}
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
val synchronizer: StateFlow<Synchronizer?> = synchronizerOrLockoutId
.flatMapLatest {
it
}
.map {
when (it) {
is InternalSynchronizerStatus.Available -> it.synchronizer
is InternalSynchronizerStatus.Lockout -> null
InternalSynchronizerStatus.NoWallet -> null
}
}.stateIn(
}
.stateIn(
walletScope, SharingStarted.WhileSubscribed(),
null
)
@ -108,54 +149,73 @@ class WalletCoordinator(context: Context) {
}
/**
* Wipes the wallet.
*
* In order for a wipe to occur, the synchronizer must be loaded already
* which would happen if the UI is collecting it.
*
* @return True if the wipe was performed and false if the wipe was not performed.
* Resets persisted data in the SDK, but preserves the wallet secret. This will cause the
* synchronizer to emit a new instance.
*/
suspend fun wipeWallet(): Boolean {
/*
* This implementation could perhaps be a little brittle due to needing to stop and start the
* synchronizer. If another client is interacting with the synchronizer at the same time,
* it isn't well defined exactly what the behavior should be.
*
* Possible enhancements to improve this:
* - Hide the synchronizer from clients; prefer to add additional APIs to WalletViewModel
* which delegate to the synchronizer
* - Add a private StateFlow to WalletCoordinator to signal internal operations which should
* cancel the synchronizer for other observers. Modify synchronizer flow to use a combine
* operator to check the private stateflow. When initiating a wipe, set that private
* StateFlow to cancel other observers of the synchronizer.
*/
@OptIn(FlowPreview::class)
fun resetSdk() {
walletScope.launch {
lockoutMutex.withLock {
val lockoutId = UUID.randomUUID()
synchronizerLockoutId.value = lockoutId
synchronizerMutex.withLock {
synchronizer.value?.let {
// There is a minor race condition here. With the right timing, it is possible
// that the collection of the Synchronizer flow is canceled during an erase.
// In such a situation, the Synchronizer would be restarted at the end of
// this method even though it shouldn't. By checking for referential equality at
// the end, we can reduce that timing gap.
val wasStarted = it.isStarted
if (wasStarted) {
it.stop()
}
synchronizerOrLockoutId
.flatMapConcat { it }
.filterIsInstance<InternalSynchronizerStatus.Lockout>()
.filter { it.id == lockoutId }
.onFirst {
synchronizerMutex.withLock {
val didDelete = Initializer.erase(
applicationContext,
ZcashNetwork.fromResources(applicationContext)
)
Initializer.erase(
applicationContext,
ZcashNetwork.fromResources(applicationContext)
)
Twig.info { "SDK erase result: $didDelete" }
}
}
if (wasStarted && synchronizer.value === it) {
it.start(walletScope)
}
return true
synchronizerLockoutId.value = null
}
}
}
return false
/**
* Wipes the wallet. Will cause the app-wide synchronizer to be reset with a new instance.
*/
@OptIn(FlowPreview::class)
fun wipeEntireWallet() {
walletScope.launch {
lockoutMutex.withLock {
val lockoutId = UUID.randomUUID()
synchronizerLockoutId.value = lockoutId
synchronizerOrLockoutId
.flatMapConcat { it }
.filterIsInstance<InternalSynchronizerStatus.Lockout>()
.filter { it.id == lockoutId }
.onFirst {
// Note that clearing the data here is non-atomic since multiple files must be modified
EncryptedPreferenceSingleton.getInstance(applicationContext).also { provider ->
EncryptedPreferenceKeys.PERSISTABLE_WALLET.putValue(provider, null)
}
StandardPreferenceSingleton.getInstance(applicationContext).also { provider ->
StandardPreferenceKeys.IS_USER_BACKUP_COMPLETE.putValue(provider, false)
}
synchronizerMutex.withLock {
val didDelete = Initializer.erase(
applicationContext,
ZcashNetwork.fromResources(applicationContext)
)
Twig.info { "SDK erase result: $didDelete" }
}
synchronizerLockoutId.value = null
}
}
}
}
}

View File

@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
@ -24,8 +23,6 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import cash.z.ecc.sdk.model.ZecRequest
import cash.z.ecc.sdk.send
import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.design.compat.FontCompat
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
import co.electriccoin.zcash.ui.design.component.GradientSurface
@ -34,8 +31,8 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.about.WrapAbout
import co.electriccoin.zcash.ui.screen.backup.WrapBackup
import co.electriccoin.zcash.ui.screen.backup.copyToClipboard
import co.electriccoin.zcash.ui.screen.home.WrapHome
import co.electriccoin.zcash.ui.screen.home.model.spendableBalance
import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
@ -45,7 +42,7 @@ import co.electriccoin.zcash.ui.screen.request.view.Request
import co.electriccoin.zcash.ui.screen.scan.WrapScan
import co.electriccoin.zcash.ui.screen.seed.view.Seed
import co.electriccoin.zcash.ui.screen.send.view.Send
import co.electriccoin.zcash.ui.screen.settings.view.Settings
import co.electriccoin.zcash.ui.screen.settings.WrapSettings
import co.electriccoin.zcash.ui.screen.support.WrapSupport
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.WrapUpdate
@ -169,6 +166,8 @@ class MainActivity : ComponentActivity() {
goSend = { navController.navigate(NAV_SEND) },
goRequest = { navController.navigate(NAV_REQUEST) }
)
WrapCheckForUpdate()
}
composable(NAV_PROFILE) {
WrapProfile(
@ -257,41 +256,6 @@ class MainActivity : ComponentActivity() {
}
}
@Composable
private fun WrapHome(
goScan: () -> Unit,
goProfile: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit
) {
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value
if (null == walletSnapshot) {
// Display loading indicator
} else {
val context = LocalContext.current
// We might eventually want to check the debuggable property of the manifest instead
// of relying on BuildConfig.
val isDebugMenuEnabled = BuildConfig.DEBUG &&
!FirebaseTestLabUtil.isFirebaseTestLab(context) &&
!EmulatorWtfUtil.isEmulatorWtf(context)
Home(
walletSnapshot,
walletViewModel.transactionSnapshot.collectAsState().value,
goScan = goScan,
goRequest = goRequest,
goSend = goSend,
goProfile = goProfile,
isDebugMenuEnabled = isDebugMenuEnabled
)
reportFullyDrawn()
WrapCheckForUpdate()
}
}
@Composable
private fun WrapCheckForUpdate() {
val updateInfo = checkUpdateViewModel.updateInfo.collectAsState().value
@ -324,33 +288,6 @@ class MainActivity : ComponentActivity() {
}
}
@Composable
private fun WrapSettings(
goBack: () -> Unit,
goWalletBackup: () -> Unit
) {
val synchronizer = walletViewModel.synchronizer.collectAsState().value
if (null == synchronizer) {
// Display loading indicator
} else {
Settings(
onBack = goBack,
onBackupWallet = goWalletBackup,
onRescanWallet = {
walletViewModel.rescanBlockchain()
}, onWipeWallet = {
walletViewModel.wipeWallet()
// If wipe ever becomes an operation to also delete the seed, then we'll also need
// to do the following to clear any retained state from onboarding (only happens if
// occurring during same session as onboarding)
// onboardingViewModel.onboardingState.goToBeginning()
// onboardingViewModel.isImporting.value = false
}
)
}
}
@Composable
private fun WrapSeed(
goBack: () -> Unit

View File

@ -25,6 +25,10 @@ class BackupState(initialState: BackupStage = BackupStage.values().first()) {
mutableState.value = current.value.getPrevious()
}
fun goToBeginning() {
mutableState.value = BackupStage.values().first()
}
fun goToSeed() {
mutableState.value = BackupStage.Seed
}

View File

@ -0,0 +1,82 @@
package co.electriccoin.zcash.ui.screen.home
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.LocalContext
import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.BuildConfig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.backup.viewmodel.BackupViewModel
import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
@Composable
internal fun MainActivity.WrapHome(
goScan: () -> Unit,
goProfile: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit
) {
WrapHome(
this,
goScan = goScan,
goProfile = goProfile,
goSend = goSend,
goRequest = goRequest
)
}
@Composable
internal fun WrapHome(
activity: ComponentActivity,
goScan: () -> Unit,
goProfile: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value
if (null == walletSnapshot) {
// Display loading indicator
} else {
val context = LocalContext.current
// We might eventually want to check the debuggable property of the manifest instead
// of relying on BuildConfig.
val isDebugMenuEnabled = BuildConfig.DEBUG &&
!FirebaseTestLabUtil.isFirebaseTestLab(context) &&
!EmulatorWtfUtil.isEmulatorWtf(context)
Home(
walletSnapshot,
walletViewModel.transactionSnapshot.collectAsState().value,
goScan = goScan,
goRequest = goRequest,
goSend = goSend,
goProfile = goProfile,
isDebugMenuEnabled = isDebugMenuEnabled,
resetSdk = {
walletViewModel.resetSdk()
},
wipeEntireWallet = {
// Although this is debug only, it still might be nice to show a warning dialog
// before performing this action
walletViewModel.wipeEntireWallet()
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
onboardingViewModel.onboardingState.goToBeginning()
onboardingViewModel.isImporting.value = false
val backupViewModel by activity.viewModels<BackupViewModel>()
backupViewModel.backupState.goToBeginning()
}
)
activity.reportFullyDrawn()
}
}

View File

@ -57,7 +57,9 @@ fun ComposablePreview() {
goProfile = {},
goSend = {},
goRequest = {},
isDebugMenuEnabled = false
isDebugMenuEnabled = false,
resetSdk = {},
wipeEntireWallet = {}
)
}
}
@ -73,10 +75,12 @@ fun Home(
goProfile: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit,
resetSdk: () -> Unit,
wipeEntireWallet: () -> Unit,
isDebugMenuEnabled: Boolean
) {
Scaffold(topBar = {
HomeTopAppBar(isDebugMenuEnabled)
HomeTopAppBar(isDebugMenuEnabled, resetSdk, wipeEntireWallet)
}) { paddingValues ->
HomeMainContent(
paddingValues,
@ -91,19 +95,23 @@ fun Home(
}
@Composable
private fun HomeTopAppBar(isDebugMenuEnabled: Boolean) {
private fun HomeTopAppBar(
isDebugMenuEnabled: Boolean,
resetSdk: () -> Unit,
wipeEntireWallet: () -> Unit
) {
SmallTopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
actions = {
if (isDebugMenuEnabled) {
DebugMenu()
DebugMenu(resetSdk, wipeEntireWallet)
}
}
)
}
@Composable
private fun DebugMenu() {
private fun DebugMenu(resetSdk: () -> Unit, wipeEntireWallet: () -> Unit) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
@ -130,6 +138,20 @@ private fun DebugMenu() {
expanded = false
}
)
DropdownMenuItem(
text = { Text("Reset SDK") },
onClick = {
resetSdk()
expanded = false
}
)
DropdownMenuItem(
text = { Text("Wipe entire wallet") },
onClick = {
wipeEntireWallet()
expanded = false
}
)
}
}

View File

@ -31,9 +31,9 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -101,10 +101,15 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
null
)
@OptIn(FlowPreview::class)
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
val walletSnapshot: StateFlow<WalletSnapshot?> = synchronizer
.filterNotNull()
.flatMapConcat { it.toWalletSnapshot() }
.flatMapLatest {
if (null == it) {
flowOf(null)
} else {
it.toWalletSnapshot()
}
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
@ -112,10 +117,15 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
)
// This is not the right API, because the transaction list could be very long and might need UI filtering
@OptIn(FlowPreview::class)
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
val transactionSnapshot: StateFlow<List<Transaction>> = synchronizer
.filterNotNull()
.flatMapConcat { it.toTransactions() }
.flatMapLatest {
if (null == it) {
flowOf(emptyList())
} else {
it.toTransactions()
}
}
.stateIn(
viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
emptyList()
@ -195,14 +205,26 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
}
/**
* This asynchronously wipes the wallet state.
* This asynchronously resets the SDK state. This is non-destructive, as SDK state can be rederived.
*
* This method only has an effect if the synchronizer currently is loaded.
* This could be used as a troubleshooting step in debugging.
*/
fun wipeWallet() {
viewModelScope.launch {
walletCoordinator.wipeWallet()
}
fun resetSdk() {
walletCoordinator.resetSdk()
}
/**
* This asynchronously wipes the entire wallet state.
*
* This is destructive, as the seed phrase is deleted along with the SDK state.
*
* This could be used as part of testing, to quickly reset the app state.
*
* A more complete reset of app state can be performed in Android Settings, as this will not
* clear application state beyond the SDK and wallet secret.
*/
fun wipeEntireWallet() {
walletCoordinator.wipeEntireWallet()
}
}

View File

@ -0,0 +1,54 @@
package co.electriccoin.zcash.ui.screen.settings
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.backup.viewmodel.BackupViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
import co.electriccoin.zcash.ui.screen.settings.view.Settings
@Composable
internal fun MainActivity.WrapSettings(
goBack: () -> Unit,
goWalletBackup: () -> Unit
) {
WrapSettings(
activity = this,
goBack = goBack,
goWalletBackup = goWalletBackup
)
}
@Composable
private fun WrapSettings(
activity: ComponentActivity,
goBack: () -> Unit,
goWalletBackup: () -> Unit
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsState().value
if (null == synchronizer) {
// Display loading indicator
} else {
Settings(
onBack = goBack,
onBackupWallet = goWalletBackup,
onRescanWallet = {
walletViewModel.rescanBlockchain()
}, onWipeWallet = {
walletViewModel.wipeEntireWallet()
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
onboardingViewModel.onboardingState.goToBeginning()
onboardingViewModel.isImporting.value = false
val backupViewModel by activity.viewModels<BackupViewModel>()
backupViewModel.backupState.goToBeginning()
}
)
}
}

View File

@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.DangerousButton
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.TertiaryButton
@ -78,7 +77,7 @@ private fun SettingsTopAppBar(onBack: () -> Unit) {
private fun SettingsMainContent(
paddingValues: PaddingValues,
onBackupWallet: () -> Unit,
onWipeWallet: () -> Unit,
@Suppress("UNUSED_PARAMETER") onWipeWallet: () -> Unit,
onRescanWallet: () -> Unit
) {
Column(
@ -86,7 +85,8 @@ private fun SettingsMainContent(
.padding(top = paddingValues.calculateTopPadding())
) {
PrimaryButton(onClick = onBackupWallet, text = stringResource(id = R.string.settings_backup))
DangerousButton(onClick = onWipeWallet, text = stringResource(id = R.string.settings_wipe))
// We have decided to not include this in settings; see overflow debug menu instead
// DangerousButton(onClick = onWipeWallet, text = stringResource(id = R.string.settings_wipe))
TertiaryButton(onClick = onRescanWallet, text = stringResource(id = R.string.settings_rescan))
}
}