[#1305] Connect shielding Multi-Trx-Submit-Error

- Closes #1305
This commit is contained in:
Honza Rychnovský 2024-03-28 10:54:51 +01:00 committed by GitHub
parent 8b58233648
commit 809820c1f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 143 additions and 59 deletions

View File

@ -1,13 +1,16 @@
package co.electriccoin.zcash.ui
import androidx.compose.runtime.Composable
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.ui.NavigationArguments.MULTIPLE_SUBMISSION_CLEAR_FORM
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_INITIAL_STAGE
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_MEMO
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_PROPOSAL
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS
@ -42,6 +45,7 @@ import co.electriccoin.zcash.ui.screen.send.ext.toSerializableAddress
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
import co.electriccoin.zcash.ui.screen.sendconfirmation.WrapSendConfirmation
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationArguments
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage
import co.electriccoin.zcash.ui.screen.settings.WrapSettings
import co.electriccoin.zcash.ui.screen.support.WrapSupport
import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate
@ -75,18 +79,19 @@ internal fun MainActivity.Navigation() {
},
goSendConfirmation = { zecSend ->
navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
handle[SEND_CONFIRM_RECIPIENT_ADDRESS] =
Json.encodeToString(
serializer = SerializableAddress.serializer(),
value = zecSend.destination.toSerializableAddress()
)
handle[SEND_CONFIRM_AMOUNT] = zecSend.amount.value
handle[SEND_CONFIRM_MEMO] = zecSend.memo.value
handle[SEND_CONFIRM_PROPOSAL] = zecSend.proposal?.toByteArray()
fillInHandleForConfirmation(handle, zecSend, SendConfirmationStage.Confirmation)
}
navController.navigateJustOnce(SEND_CONFIRMATION)
},
goSettings = { navController.navigateJustOnce(SETTINGS) },
goMultiTrxSubmissionFailure = {
// Ultimately we could approach reworking the MultipleTrxFailure screen into a separate
// navigation endpoint
navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
fillInHandleForConfirmation(handle, null, SendConfirmationStage.MultipleTrxFailure)
}
navController.navigateJustOnce(SEND_CONFIRMATION)
},
sendArguments =
SendArguments(
recipientAddress =
@ -195,21 +200,31 @@ internal fun MainActivity.Navigation() {
navController.popBackStackJustOnce(SEND_CONFIRMATION)
},
goHome = { navController.navigateJustOnce(HOME) },
arguments =
SendConfirmationArguments.fromSavedStateHandle(backStackEntry.savedStateHandle).also {
// Remove SendConfirmation screen arguments passed from the Send screen if some exist
// after we use them
backStackEntry.savedStateHandle.remove<String>(SEND_CONFIRM_RECIPIENT_ADDRESS)
backStackEntry.savedStateHandle.remove<Long>(SEND_CONFIRM_AMOUNT)
backStackEntry.savedStateHandle.remove<String>(SEND_CONFIRM_MEMO)
backStackEntry.savedStateHandle.remove<ByteArray>(SEND_CONFIRM_PROPOSAL)
}
arguments = SendConfirmationArguments.fromSavedStateHandle(backStackEntry.savedStateHandle)
)
}
}
}
}
private fun fillInHandleForConfirmation(
handle: SavedStateHandle,
zecSend: ZecSend?,
initialStage: SendConfirmationStage
) {
if (zecSend != null) {
handle[SEND_CONFIRM_RECIPIENT_ADDRESS] =
Json.encodeToString(
serializer = SerializableAddress.serializer(),
value = zecSend.destination.toSerializableAddress()
)
handle[SEND_CONFIRM_AMOUNT] = zecSend.amount.value
handle[SEND_CONFIRM_MEMO] = zecSend.memo.value
handle[SEND_CONFIRM_PROPOSAL] = zecSend.proposal?.toByteArray()
}
handle[SEND_CONFIRM_INITIAL_STAGE] = initialStage.toStringName()
}
private fun NavHostController.navigateJustOnce(
route: String,
navOptionsBuilder: (NavOptionsBuilder.() -> Unit)? = null
@ -245,6 +260,7 @@ object NavigationArguments {
const val SEND_CONFIRM_AMOUNT = "send_confirm_amount"
const val SEND_CONFIRM_MEMO = "send_confirm_memo"
const val SEND_CONFIRM_PROPOSAL = "send_confirm_proposal"
const val SEND_CONFIRM_INITIAL_STAGE = "send_confirm_initial_stage"
const val MULTIPLE_SUBMISSION_CLEAR_FORM = "multiple_submission_clear_form"
}

View File

@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi
@ -21,6 +22,8 @@ import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
import co.electriccoin.zcash.ui.screen.balances.view.Balances
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
@ -32,9 +35,12 @@ import org.jetbrains.annotations.VisibleForTesting
internal fun WrapBalances(
activity: ComponentActivity,
goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit,
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val createTransactionsViewModel by activity.viewModels<CreateTransactionsViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
@ -51,8 +57,10 @@ internal fun WrapBalances(
val settingsViewModel by activity.viewModels<SettingsViewModel>()
WrapBalances(
goSettings = goSettings,
checkUpdateViewModel = checkUpdateViewModel,
createTransactionsViewModel = createTransactionsViewModel,
goSettings = goSettings,
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure,
spendingKey = spendingKey,
settingsViewModel = settingsViewModel,
synchronizer = synchronizer,
@ -66,8 +74,10 @@ const val DEFAULT_SHIELDING_THRESHOLD = 100000L
@VisibleForTesting
@Suppress("LongParameterList", "LongMethod")
internal fun WrapBalances(
goSettings: () -> Unit,
checkUpdateViewModel: CheckUpdateViewModel,
createTransactionsViewModel: CreateTransactionsViewModel,
goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit,
settingsViewModel: SettingsViewModel,
spendingKey: UnifiedSpendingKey?,
synchronizer: Synchronizer?,
@ -98,14 +108,14 @@ internal fun WrapBalances(
val (isShowingErrorDialog, setShowErrorDialog) = rememberSaveable { mutableStateOf(false) }
suspend fun showShieldingError(error: Throwable?) {
Twig.error { "Shielding proposal failed with: $error" }
suspend fun showShieldingError(errorMessage: String?) {
Twig.error { "Shielding proposal failed with: $errorMessage" }
// Adding the extra delay before notifying UI for a better UX
@Suppress("MagicNumber")
delay(1500)
setShieldState(ShieldState.Failed(error?.message ?: ""))
setShieldState(ShieldState.Failed(errorMessage ?: ""))
setShowErrorDialog(true)
}
@ -143,26 +153,35 @@ internal fun WrapBalances(
if (newProposal == null) {
showShieldingError(null)
} else {
// TODO [#1294]: Add Send.Multiple-Trx-Failed screen
// TODO [#1294]: Note that the following processing is not entirely correct and will be
// reworked
// TODO [#1294]: https://github.com/Electric-Coin-Company/zashi-android/issues/1294
runCatching {
synchronizer.createProposedTransactions(
proposal = newProposal,
usk = spendingKey
).collect {
Twig.info { "Printing only for now. Will be reworked. Result: $it" }
val result =
createTransactionsViewModel.runCreateTransactions(
synchronizer = synchronizer,
spendingKey = spendingKey,
proposal = newProposal
)
when (result) {
SubmitResult.Success -> {
Twig.info { "Shielding transaction done successfully" }
setShieldState(ShieldState.None)
// Triggering transaction history refresh to be notified about the newly created
// transaction asap
(synchronizer as SdkSynchronizer).refreshTransactions()
// We could consider notifying UI with a change to emphasize the shielding action
// was successful, or we could switch the selected tab to Account
}
is SubmitResult.SimpleTrxFailure -> {
Twig.warn { "Shielding transaction failed" }
showShieldingError(result.errorDescription)
}
is SubmitResult.MultipleTrxFailure -> {
Twig.warn { "Shielding failed with multi-transactions-submission-error handling" }
goMultiTrxSubmissionFailure()
}
}.onSuccess {
Twig.debug { "Shielding transaction event" }
setShieldState(ShieldState.None)
}.onFailure {
showShieldingError(it)
}
}
}.onFailure {
showShieldingError(it)
showShieldingError(it.message)
}
}
},

View File

@ -29,6 +29,7 @@ internal fun MainActivity.WrapHome(
onPageChange: (HomeScreenIndex) -> Unit,
goBack: () -> Unit,
goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit,
goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
sendArguments: SendArguments
@ -40,6 +41,7 @@ internal fun MainActivity.WrapHome(
goScan = goScan,
goSendConfirmation = goSendConfirmation,
goSettings = goSettings,
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure,
sendArguments = sendArguments
)
}
@ -50,6 +52,7 @@ internal fun WrapHome(
activity: ComponentActivity,
goBack: () -> Unit,
goSettings: () -> Unit,
goMultiTrxSubmissionFailure: () -> Unit,
goScan: () -> Unit,
goSendConfirmation: (ZecSend) -> Unit,
onPageChange: (HomeScreenIndex) -> Unit,
@ -127,7 +130,8 @@ internal fun WrapHome(
screenContent = {
WrapBalances(
activity = activity,
goSettings = goSettings
goSettings = goSettings,
goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure
)
}
)

View File

@ -33,7 +33,7 @@ import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationAr
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult
import co.electriccoin.zcash.ui.screen.sendconfirmation.view.SendConfirmation
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.SendConfirmationViewModel
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo
import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType
import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel
@ -50,7 +50,7 @@ internal fun MainActivity.WrapSendConfirmation(
) {
val walletViewModel by viewModels<WalletViewModel>()
val sendViewModel by viewModels<SendConfirmationViewModel>()
val createTransactionsViewModel by viewModels<CreateTransactionsViewModel>()
val viewModel by viewModels<SupportViewModel>()
@ -65,7 +65,7 @@ internal fun MainActivity.WrapSendConfirmation(
arguments = arguments,
goBack = goBack,
goHome = goHome,
sendViewModel = sendViewModel,
createTransactionsViewModel = createTransactionsViewModel,
spendingKey = spendingKey,
supportMessage = supportMessage,
synchronizer = synchronizer,
@ -80,7 +80,7 @@ internal fun WrapSendConfirmation(
arguments: SendConfirmationArguments,
goBack: (clearForm: Boolean) -> Unit,
goHome: () -> Unit,
sendViewModel: SendConfirmationViewModel,
createTransactionsViewModel: CreateTransactionsViewModel,
spendingKey: UnifiedSpendingKey?,
supportMessage: SupportInfo?,
synchronizer: Synchronizer?,
@ -89,17 +89,22 @@ internal fun WrapSendConfirmation(
val snackbarHostState = remember { SnackbarHostState() }
val zecSend by rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(arguments.toZecSend()) }
// Because of the [zecSend] has the same Saver as on the Send screen, we do not expect this to be ever null
checkNotNull(zecSend)
val zecSend by rememberSaveable(stateSaver = ZecSend.Saver) {
mutableStateOf(
if (arguments.hasValidZecSend()) {
arguments.toZecSend()
} else {
null
}
)
}
val (stage, setStage) =
rememberSaveable(stateSaver = SendConfirmationStage.Saver) {
mutableStateOf(SendConfirmationStage.Confirmation)
mutableStateOf(arguments.initialStage ?: SendConfirmationStage.Confirmation)
}
val submissionResults = sendViewModel.submissions.collectAsState().value.toImmutableList()
val submissionResults = createTransactionsViewModel.submissions.collectAsState().value.toImmutableList()
val onBackAction = {
when (stage) {
@ -124,7 +129,7 @@ internal fun WrapSendConfirmation(
SendConfirmation(
stage = stage,
onStageChange = setStage,
zecSend = zecSend!!,
zecSend = zecSend,
submissionResults = submissionResults,
snackbarHostState = snackbarHostState,
onBack = onBackAction,
@ -167,7 +172,7 @@ internal fun WrapSendConfirmation(
checkNotNull(newZecSend.proposal)
val result =
sendViewModel.runSending(
createTransactionsViewModel.runCreateTransactions(
synchronizer = synchronizer,
spendingKey = spendingKey,
proposal = newZecSend.proposal!!

View File

@ -14,6 +14,7 @@ data class SendConfirmationArguments(
val amount: Long?,
val memo: String?,
val proposal: ByteArray?,
val initialStage: SendConfirmationStage?,
) {
companion object {
internal fun fromSavedStateHandle(savedStateHandle: SavedStateHandle) =
@ -25,9 +26,23 @@ data class SendConfirmationArguments(
amount = savedStateHandle.get<Long>(NavigationArguments.SEND_CONFIRM_AMOUNT),
memo = savedStateHandle.get<String>(NavigationArguments.SEND_CONFIRM_MEMO),
proposal = savedStateHandle.get<ByteArray>(NavigationArguments.SEND_CONFIRM_PROPOSAL),
)
initialStage =
SendConfirmationStage
.fromStringName(savedStateHandle.get<String>(NavigationArguments.SEND_CONFIRM_INITIAL_STAGE))
).also {
// Remove SendConfirmation screen arguments passed from the other screens if some exist
savedStateHandle.remove<String>(NavigationArguments.SEND_CONFIRM_RECIPIENT_ADDRESS)
savedStateHandle.remove<Long>(NavigationArguments.SEND_CONFIRM_AMOUNT)
savedStateHandle.remove<String>(NavigationArguments.SEND_CONFIRM_MEMO)
savedStateHandle.remove<ByteArray>(NavigationArguments.SEND_CONFIRM_PROPOSAL)
savedStateHandle.remove<String>(NavigationArguments.SEND_CONFIRM_INITIAL_STAGE)
}
}
internal fun hasValidZecSend() =
this.address != null &&
this.amount != null
internal fun toZecSend() =
ZecSend(
destination = address?.toWalletAddress() ?: error("Address null"),

View File

@ -7,12 +7,22 @@ sealed class SendConfirmationStage {
data object Sending : SendConfirmationStage()
data class Failure(val error: String) : SendConfirmationStage()
data class Failure(val error: String?) : SendConfirmationStage()
data object MultipleTrxFailure : SendConfirmationStage()
data object MultipleTrxFailureReported : SendConfirmationStage()
fun toStringName(): String {
return when (this) {
Confirmation -> TYPE_CONFIRMATION
is Failure -> TYPE_FAILURE
MultipleTrxFailure -> TYPE_MULTIPLE_TRX_FAILURE
MultipleTrxFailureReported -> TYPE_MULTIPLE_TRX_FAILURE_REPORTED
Sending -> TYPE_SENDING
}
}
companion object {
private const val TYPE_CONFIRMATION = "confirmation" // $NON-NLS
private const val TYPE_SENDING = "sending" // $NON-NLS
@ -52,7 +62,7 @@ sealed class SendConfirmationStage {
Sending -> saverMap[KEY_TYPE] = TYPE_SENDING
is Failure -> {
saverMap[KEY_TYPE] = TYPE_FAILURE
saverMap[KEY_ERROR] = this.error
saverMap[KEY_ERROR] = this.error ?: ""
}
is MultipleTrxFailure -> saverMap[KEY_TYPE] = TYPE_MULTIPLE_TRX_FAILURE
is MultipleTrxFailureReported -> saverMap[KEY_TYPE] = TYPE_MULTIPLE_TRX_FAILURE_REPORTED
@ -60,5 +70,17 @@ sealed class SendConfirmationStage {
return saverMap
}
fun fromStringName(stringName: String?): SendConfirmationStage {
return when (stringName) {
TYPE_CONFIRMATION -> Confirmation
TYPE_SENDING -> Sending
// Add the String error parameter storing and retrieving
TYPE_FAILURE -> Failure(null)
TYPE_MULTIPLE_TRX_FAILURE -> MultipleTrxFailure
TYPE_MULTIPLE_TRX_FAILURE_REPORTED -> MultipleTrxFailureReported
else -> Confirmation
}
}
}
}

View File

@ -134,7 +134,7 @@ fun SendConfirmation(
snackbarHostState: SnackbarHostState,
stage: SendConfirmationStage,
submissionResults: ImmutableList<TransactionSubmitResult>,
zecSend: ZecSend,
zecSend: ZecSend?,
) {
Scaffold(
topBar = { SendConfirmationTopAppBar(onBack, stage) },
@ -197,11 +197,14 @@ private fun SendConfirmationMainContent(
onStageChange: (SendConfirmationStage) -> Unit,
stage: SendConfirmationStage,
submissionResults: ImmutableList<TransactionSubmitResult>,
zecSend: ZecSend,
zecSend: ZecSend?,
modifier: Modifier = Modifier,
) {
when (stage) {
SendConfirmationStage.Confirmation, SendConfirmationStage.Sending, is SendConfirmationStage.Failure -> {
if (zecSend == null) {
error("Unexpected ZecSend value: $zecSend")
}
SendConfirmationContent(
zecSend = zecSend,
onBack = onBack,
@ -371,7 +374,7 @@ fun SendConfirmationActionButtons(
@Suppress("UNUSED_PARAMETER")
private fun SendFailure(
onDone: () -> Unit,
reason: String,
reason: String?,
) {
// Once we ensure that the [reason] contains a localized message, we can leverage it for the UI prompt

View File

@ -10,12 +10,12 @@ import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult
import kotlinx.coroutines.flow.MutableStateFlow
class SendConfirmationViewModel(application: Application) : AndroidViewModel(application) {
class CreateTransactionsViewModel(application: Application) : AndroidViewModel(application) {
// Technically this value will not survive process dead, but will survive all possible configuration changes
// Possible solution would be storing the value within [SavedStateHandle]
val submissions: MutableStateFlow<List<TransactionSubmitResult>> = MutableStateFlow(emptyList())
suspend fun runSending(
suspend fun runCreateTransactions(
synchronizer: Synchronizer,
spendingKey: UnifiedSpendingKey,
proposal: Proposal