[#1367] Send 0 funds only within shielded transaction
- Closes #1367 - Plus slightly refactored SendForm composable to two functions - Changelog update
This commit is contained in:
parent
b00c807df1
commit
fb5d446bab
|
@ -9,6 +9,9 @@ directly impact users rather than highlighting other key architectural updates.*
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Sending zero funds is allowed only for shielded recipient address type
|
||||||
|
|
||||||
## [0.2.0 (609)] - 2024-04-18
|
## [0.2.0 (609)] - 2024-04-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -128,7 +128,7 @@ class SendViewTestSetup(
|
||||||
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
||||||
},
|
},
|
||||||
setAmountState = {},
|
setAmountState = {},
|
||||||
amountState = AmountState.new(context, "", monetarySeparators),
|
amountState = AmountState.new(context, monetarySeparators, "", false),
|
||||||
setMemoState = {},
|
setMemoState = {},
|
||||||
memoState = MemoState.new(""),
|
memoState = MemoState.new(""),
|
||||||
walletRestoringState = WalletRestoringState.NONE,
|
walletRestoringState = WalletRestoringState.NONE,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
@ -21,6 +22,7 @@ import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.model.ZecSend
|
import cash.z.ecc.android.sdk.model.ZecSend
|
||||||
import cash.z.ecc.android.sdk.model.proposeSend
|
import cash.z.ecc.android.sdk.model.proposeSend
|
||||||
import cash.z.ecc.android.sdk.model.toZecString
|
import cash.z.ecc.android.sdk.model.toZecString
|
||||||
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
import co.electriccoin.zcash.spackle.Twig
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||||
|
@ -95,7 +97,7 @@ internal fun WrapSend(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList", "LongMethod")
|
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod")
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@Composable
|
@Composable
|
||||||
internal fun WrapSend(
|
internal fun WrapSend(
|
||||||
|
@ -140,14 +142,28 @@ internal fun WrapSend(
|
||||||
// Amount computation:
|
// Amount computation:
|
||||||
val (amountState, setAmountState) =
|
val (amountState, setAmountState) =
|
||||||
rememberSaveable(stateSaver = AmountState.Saver) {
|
rememberSaveable(stateSaver = AmountState.Saver) {
|
||||||
|
// Default amount state
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
AmountState.new(
|
AmountState.new(
|
||||||
context = context,
|
context = context,
|
||||||
value = zecSend?.amount?.toZecString() ?: "",
|
value = zecSend?.amount?.toZecString() ?: "",
|
||||||
monetarySeparators = monetarySeparators
|
monetarySeparators = monetarySeparators,
|
||||||
|
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// New amount state based on the recipient address type (e.g. shielded supports zero funds sending and
|
||||||
|
// transparent not)
|
||||||
|
LaunchedEffect(key1 = recipientAddressState) {
|
||||||
|
setAmountState(
|
||||||
|
AmountState.new(
|
||||||
|
context = context,
|
||||||
|
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
|
||||||
|
monetarySeparators = monetarySeparators,
|
||||||
|
value = amountState.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Memo computation:
|
// Memo computation:
|
||||||
val (memoState, setMemoState) =
|
val (memoState, setMemoState) =
|
||||||
|
@ -155,12 +171,12 @@ internal fun WrapSend(
|
||||||
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: ""))
|
mutableStateOf(MemoState.new(zecSend?.memo?.value ?: ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clearing form if required form the previous navigation destination
|
// Clearing form from the previous navigation destination if required
|
||||||
if (sendArguments?.clearForm == true) {
|
if (sendArguments?.clearForm == true) {
|
||||||
setSendStage(SendStage.Form)
|
setSendStage(SendStage.Form)
|
||||||
setZecSend(null)
|
setZecSend(null)
|
||||||
setRecipientAddressState(RecipientAddressState.new("", null))
|
setRecipientAddressState(RecipientAddressState.new("", null))
|
||||||
setAmountState(AmountState.new(context, "", monetarySeparators))
|
setAmountState(AmountState.new(context, monetarySeparators, "", false))
|
||||||
setMemoState(MemoState.new(""))
|
setMemoState(MemoState.new(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,9 @@ sealed class AmountState(
|
||||||
companion object {
|
companion object {
|
||||||
fun new(
|
fun new(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
monetarySeparators: MonetarySeparators,
|
||||||
value: String,
|
value: String,
|
||||||
monetarySeparators: MonetarySeparators
|
isTransparentRecipient: Boolean
|
||||||
): AmountState {
|
): AmountState {
|
||||||
// Validate raw input string
|
// Validate raw input string
|
||||||
val validated =
|
val validated =
|
||||||
|
@ -41,11 +42,11 @@ sealed class AmountState(
|
||||||
// Convert the input to Zatoshi type-safe amount representation
|
// Convert the input to Zatoshi type-safe amount representation
|
||||||
val zatoshi = (Zatoshi.fromZecString(context, value, monetarySeparators))
|
val zatoshi = (Zatoshi.fromZecString(context, value, monetarySeparators))
|
||||||
|
|
||||||
// Note that the 0 funds sending is supported for sending a memo-only transaction
|
// Note that the zero funds sending is supported for sending a memo-only shielded transaction
|
||||||
return if (zatoshi == null) {
|
return when {
|
||||||
Invalid(value)
|
(zatoshi == null) -> Invalid(value)
|
||||||
} else {
|
(zatoshi.value == 0L && isTransparentRecipient) -> Invalid(value)
|
||||||
Valid(value, zatoshi)
|
else -> Valid(value, zatoshi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -264,8 +264,6 @@ private fun SendForm(
|
||||||
hasCameraFeature: Boolean,
|
hasCameraFeature: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
// TODO [#1171]: Remove default MonetarySeparators locale
|
// TODO [#1171]: Remove default MonetarySeparators locale
|
||||||
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
|
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
|
||||||
val monetarySeparators = MonetarySeparators.current(Locale.US)
|
val monetarySeparators = MonetarySeparators.current(Locale.US)
|
||||||
|
@ -305,16 +303,17 @@ private fun SendForm(
|
||||||
|
|
||||||
SendFormAmountTextField(
|
SendFormAmountTextField(
|
||||||
amountSate = amountState,
|
amountSate = amountState,
|
||||||
setAmountState = setAmountState,
|
|
||||||
monetarySeparators = monetarySeparators,
|
|
||||||
focusManager = focusManager,
|
focusManager = focusManager,
|
||||||
walletSnapshot = walletSnapshot,
|
|
||||||
imeAction =
|
imeAction =
|
||||||
if (recipientAddressState.type == AddressType.Transparent) {
|
if (recipientAddressState.type == AddressType.Transparent) {
|
||||||
ImeAction.Done
|
ImeAction.Done
|
||||||
} else {
|
} else {
|
||||||
ImeAction.Next
|
ImeAction.Next
|
||||||
}
|
},
|
||||||
|
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
|
||||||
|
monetarySeparators = monetarySeparators,
|
||||||
|
setAmountState = setAmountState,
|
||||||
|
walletSnapshot = walletSnapshot,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
|
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
|
||||||
|
@ -342,68 +341,83 @@ private fun SendForm(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
// Common conditions continuously checked for validity
|
SendButton(amountState, memoState, monetarySeparators, onCreateZecSend, recipientAddressState, walletSnapshot)
|
||||||
val sendButtonEnabled =
|
}
|
||||||
recipientAddressState.type !is AddressType.Invalid &&
|
}
|
||||||
recipientAddressState.address.isNotEmpty() &&
|
|
||||||
amountState is AmountState.Valid &&
|
|
||||||
amountState.value.isNotBlank() &&
|
|
||||||
walletSnapshot.canSpend(amountState.zatoshi) &&
|
|
||||||
// A valid memo is necessary only for non-transparent recipient
|
|
||||||
(recipientAddressState.type == AddressType.Transparent || memoState is MemoState.Correct)
|
|
||||||
|
|
||||||
Column(
|
@Composable
|
||||||
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular),
|
@Suppress("LongParameterList")
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
fun SendButton(
|
||||||
) {
|
amountState: AmountState,
|
||||||
PrimaryButton(
|
memoState: MemoState,
|
||||||
onClick = {
|
monetarySeparators: MonetarySeparators,
|
||||||
// SDK side validations
|
onCreateZecSend: (ZecSend) -> Unit,
|
||||||
val zecSendValidation =
|
recipientAddressState: RecipientAddressState,
|
||||||
ZecSendExt.new(
|
walletSnapshot: WalletSnapshot,
|
||||||
context = context,
|
) {
|
||||||
destinationString = recipientAddressState.address,
|
val context = LocalContext.current
|
||||||
zecString = amountState.value,
|
|
||||||
// Take memo for a valid non-transparent receiver only
|
|
||||||
memoString =
|
|
||||||
if (recipientAddressState.type == AddressType.Transparent) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
memoState.text
|
|
||||||
},
|
|
||||||
monetarySeparators = monetarySeparators
|
|
||||||
)
|
|
||||||
|
|
||||||
when (zecSendValidation) {
|
// Common conditions continuously checked for validity
|
||||||
is ZecSendExt.ZecSendValidation.Valid -> onCreateZecSend(zecSendValidation.zecSend)
|
val sendButtonEnabled =
|
||||||
is ZecSendExt.ZecSendValidation.Invalid -> {
|
recipientAddressState.type !is AddressType.Invalid &&
|
||||||
// We do not expect this validation to fail, so logging is enough here
|
recipientAddressState.address.isNotEmpty() &&
|
||||||
// An error popup could be reasonable here as well
|
amountState is AmountState.Valid &&
|
||||||
Twig.warn { "Send failed with: ${zecSendValidation.validationErrors}" }
|
amountState.value.isNotBlank() &&
|
||||||
}
|
walletSnapshot.canSpend(amountState.zatoshi) &&
|
||||||
|
// A valid memo is necessary only for non-transparent recipient
|
||||||
|
(recipientAddressState.type == AddressType.Transparent || memoState is MemoState.Correct)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
PrimaryButton(
|
||||||
|
onClick = {
|
||||||
|
// SDK side validations
|
||||||
|
val zecSendValidation =
|
||||||
|
ZecSendExt.new(
|
||||||
|
context = context,
|
||||||
|
destinationString = recipientAddressState.address,
|
||||||
|
zecString = amountState.value,
|
||||||
|
// Take memo for a valid non-transparent receiver only
|
||||||
|
memoString =
|
||||||
|
if (recipientAddressState.type == AddressType.Transparent) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
memoState.text
|
||||||
|
},
|
||||||
|
monetarySeparators = monetarySeparators
|
||||||
|
)
|
||||||
|
|
||||||
|
when (zecSendValidation) {
|
||||||
|
is ZecSendExt.ZecSendValidation.Valid -> onCreateZecSend(zecSendValidation.zecSend)
|
||||||
|
is ZecSendExt.ZecSendValidation.Invalid -> {
|
||||||
|
// We do not expect this validation to fail, so logging is enough here
|
||||||
|
// An error popup could be reasonable here as well
|
||||||
|
Twig.warn { "Send failed with: ${zecSendValidation.validationErrors}" }
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
text = stringResource(id = R.string.send_create),
|
},
|
||||||
enabled = sendButtonEnabled,
|
text = stringResource(id = R.string.send_create),
|
||||||
modifier =
|
enabled = sendButtonEnabled,
|
||||||
Modifier
|
modifier =
|
||||||
.testTag(SendTag.SEND_FORM_BUTTON)
|
Modifier
|
||||||
.fillMaxWidth()
|
.testTag(SendTag.SEND_FORM_BUTTON)
|
||||||
)
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||||
|
|
||||||
BodySmall(
|
BodySmall(
|
||||||
text =
|
text =
|
||||||
stringResource(
|
stringResource(
|
||||||
id = R.string.send_fee,
|
id = R.string.send_fee,
|
||||||
// TODO [#1047]: Representing Zatoshi amount
|
// TODO [#1047]: Representing Zatoshi amount
|
||||||
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
|
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
|
||||||
Zatoshi(DEFAULT_LESS_THAN_FEE).toZecString()
|
Zatoshi(DEFAULT_LESS_THAN_FEE).toZecString()
|
||||||
),
|
),
|
||||||
textFontWeight = FontWeight.SemiBold
|
textFontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -496,10 +510,11 @@ fun SendFormAddressTextField(
|
||||||
fun SendFormAmountTextField(
|
fun SendFormAmountTextField(
|
||||||
amountSate: AmountState,
|
amountSate: AmountState,
|
||||||
focusManager: FocusManager,
|
focusManager: FocusManager,
|
||||||
|
imeAction: ImeAction,
|
||||||
|
isTransparentRecipient: Boolean,
|
||||||
monetarySeparators: MonetarySeparators,
|
monetarySeparators: MonetarySeparators,
|
||||||
setAmountState: (AmountState) -> Unit,
|
setAmountState: (AmountState) -> Unit,
|
||||||
walletSnapshot: WalletSnapshot,
|
walletSnapshot: WalletSnapshot,
|
||||||
imeAction: ImeAction,
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
@ -540,7 +555,14 @@ fun SendFormAmountTextField(
|
||||||
FormTextField(
|
FormTextField(
|
||||||
value = amountSate.value,
|
value = amountSate.value,
|
||||||
onValueChange = { newValue ->
|
onValueChange = { newValue ->
|
||||||
setAmountState(AmountState.new(context, newValue, monetarySeparators))
|
setAmountState(
|
||||||
|
AmountState.new(
|
||||||
|
context = context,
|
||||||
|
value = newValue,
|
||||||
|
monetarySeparators = monetarySeparators,
|
||||||
|
isTransparentRecipient = isTransparentRecipient
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
error = amountError,
|
error = amountError,
|
||||||
|
|
Loading…
Reference in New Issue