[#1285] Adopt proposal API

* Adopt Zcash SDK v2.0.8-SNAPSHOT

* [#1285] Adopt proposal API

- Closes #1285
- Manually tested and the updated send and shield features work as expected
This commit is contained in:
Honza Rychnovský 2024-03-15 10:05:40 +01:00 committed by GitHub
parent 8e495a542f
commit fe5236fdae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 147 additions and 53 deletions

View File

@ -189,7 +189,7 @@ ZCASH_BIP39_VERSION=1.0.7
ZXING_VERSION=3.5.2
# WARNING: Ensure a non-snapshot version is used before releasing to production.
ZCASH_SDK_VERSION=2.0.7
ZCASH_SDK_VERSION=2.0.8-SNAPSHOT
# Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application.

View File

@ -2,6 +2,7 @@ package cash.z.ecc.sdk.fixture
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZecSend
@ -13,9 +14,13 @@ object ZecSendFixture {
val AMOUNT = Zatoshi(123)
val MEMO = MemoFixture.new()
// Null until we figure out how to proper test this
val PROPOSAL = null
suspend fun new(
address: String = ADDRESS,
amount: Zatoshi = AMOUNT,
memo: Memo = MEMO
) = ZecSend(WalletAddress.Unified.new(address), amount, memo)
memo: Memo = MEMO,
proposal: Proposal? = PROPOSAL
) = ZecSend(WalletAddress.Unified.new(address), amount, memo, proposal)
}

View File

@ -120,8 +120,8 @@ class SendViewTestSetup(
),
sendStage = sendStage,
onSendStageChange = setSendStage,
onCreateZecSend = setZecSend,
zecSend = zecSend,
onZecSendChange = setZecSend,
focusManager = LocalFocusManager.current,
onBack = onBackAction,
onSettings = { onSettingsCount.incrementAndGet() },

View File

@ -25,6 +25,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import java.util.Locale
import kotlin.test.Ignore
class SendViewIntegrationTest {
@get:Rule
@ -52,8 +53,11 @@ class SendViewIntegrationTest {
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
private val monetarySeparators = MonetarySeparators.current(Locale.US)
// TODO [#1260]: Cover Send screens UI with tests
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
@Test
@MediumTest
@Ignore("Disabled as the entire Send flow will be reworked and the test align after it")
fun send_screens_values_state_restoration() {
val restorationTester = StateRestorationTester(composeTestRule)

View File

@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.common.model
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
@ -19,16 +18,17 @@ data class WalletSnapshot(
val progress: PercentDecimal,
val synchronizerError: SynchronizerError?
) {
// Note that both [hasSaplingFunds] and [hasTransparentFunds] checks are not entirely correct - they do not
// calculate the resulting fee using the new Proposal API. It's fine for now, but it's subject to improvement
// later once we figure out how to handle it in such cases.
// Note: the wallet is effectively empty if it cannot cover the miner's fee
val hasSaplingFunds =
saplingBalance.available.value >
(ZcashSdk.MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.00001
val hasSaplingBalance = saplingBalance.total.value > 0
val hasSaplingFunds = saplingBalance.available.value > 0L
val hasSaplingBalance = saplingBalance.total.value > 0L
// Note: the wallet's transparent balance is effectively empty if it cannot cover the miner's fee
val hasTransparentFunds =
transparentBalance.value >
(ZcashSdk.MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.00001
val hasTransparentFunds = transparentBalance.value > 0L
val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasSaplingFunds
}

View File

@ -11,6 +11,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
@ -59,9 +60,11 @@ internal fun WrapBalances(
)
}
const val DEFAULT_SHIELDING_THRESHOLD = 100000L
@Composable
@VisibleForTesting
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LongMethod")
internal fun WrapBalances(
goSettings: () -> Unit,
checkUpdateViewModel: CheckUpdateViewModel,
@ -95,6 +98,17 @@ internal fun WrapBalances(
val (isShowingErrorDialog, setShowErrorDialog) = rememberSaveable { mutableStateOf(false) }
suspend fun showShieldingError(error: Throwable?) {
Twig.error { "Shielding proposal failed with: $error" }
// Adding the extra delay before notifying UI for a better UX
@Suppress("MagicNumber")
delay(1500)
setShieldState(ShieldState.Failed(error?.message ?: ""))
setShowErrorDialog(true)
}
if (null == synchronizer || null == walletSnapshot || null == spendingKey) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
@ -113,25 +127,41 @@ internal fun WrapBalances(
setShieldState(ShieldState.Running)
Twig.debug { "Shielding transparent funds" }
// Using empty string for memo to clear the default memo prefix value defined in the SDK
runCatching {
// TODO [#1285]: Adopt proposal API
// TODO [#1285]: https://github.com/Electric-Coin-Company/zashi-android/issues/1285
@Suppress("deprecation")
synchronizer.shieldFunds(spendingKey, "")
synchronizer.proposeShielding(
account = spendingKey.account,
shieldingThreshold = Zatoshi(DEFAULT_SHIELDING_THRESHOLD),
// Using empty string for memo to clear the default memo prefix value defined in the SDK
memo = "",
// Using null will select whichever of the account's trans. receivers has funds to shield
transparentReceiver = null
)
}.onSuccess { newProposal ->
Twig.debug { "Shielding proposal result: ${newProposal?.toPrettyString()}" }
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
)
}.onSuccess {
Twig.debug { "Shielding transaction event" }
setShieldState(ShieldState.None)
}.onFailure {
showShieldingError(null)
}
}
}.onFailure {
showShieldingError(it)
}
.onSuccess {
Twig.info { "Shielding transaction id:$it submitted successfully" }
setShieldState(ShieldState.None)
}
.onFailure {
Twig.error(it) { "Shielding transaction submission failed with: ${it.message}" }
// Adding extra delay before notifying UI for a better UX
@Suppress("MagicNumber")
delay(1500)
setShieldState(ShieldState.Failed(it.message ?: ""))
setShowErrorDialog(true)
}
}
},
shieldState = shieldState,

View File

@ -19,8 +19,8 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.proposeSend
import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.sdk.extension.send
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
@ -193,7 +193,25 @@ internal fun WrapSend(
sendStage = sendStage,
onSendStageChange = setSendStage,
zecSend = zecSend,
onZecSendChange = setZecSend,
onCreateZecSend = { newZecSend ->
scope.launch {
Twig.debug { "Getting send transaction proposal" }
runCatching {
synchronizer.proposeSend(spendingKey.account, newZecSend)
}
.onSuccess { proposal ->
Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" }
setSendStage(SendStage.Confirmation)
setZecSend(newZecSend.copy(proposal = proposal))
}
.onFailure {
Twig.error(it) { "Transaction proposal failed" }
// TODO [#1161]: Remove Send-Success and rework Send-Failure
// TODO [#1161]: https://github.com/Electric-Coin-Company/zashi-android/issues/1161
setSendStage(SendStage.SendFailure(it.message ?: ""))
}
}
},
focusManager = focusManager,
onBack = onBackAction,
onSettings = goSettings,
@ -210,16 +228,26 @@ internal fun WrapSend(
)
}
},
onCreateAndSend = {
onCreateAndSend = { newZecSend ->
scope.launch {
Twig.debug { "Sending transaction" }
runCatching { synchronizer.send(spendingKey, it) }
// 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 {
// The not-null assertion operator is necessary here even if we check its nullability before
// due to: "Smart cast to 'Proposal' is impossible, because 'zecSend.proposal' is a public API
// property declared in different module
// See more details on the Kotlin forum
checkNotNull(newZecSend.proposal)
synchronizer.createProposedTransactions(newZecSend.proposal!!, spendingKey)
}
.onSuccess {
setSendStage(SendStage.SendSuccessful)
Twig.debug { "Transaction id:$it submitted successfully" }
}
.onFailure {
Twig.debug { "Transaction submission failed with: $it." }
Twig.error(it) { "Transaction submission failed" }
setSendStage(SendStage.SendFailure(it.message ?: ""))
}
}

View File

@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.screen.send.ext
import androidx.compose.runtime.saveable.mapSaver
import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZecSend
@ -10,6 +11,7 @@ import kotlinx.coroutines.runBlocking
private const val KEY_ADDRESS = "address" // $NON-NLS
private const val KEY_AMOUNT = "amount" // $NON-NLS
private const val KEY_MEMO = "memo" // $NON-NLS
private const val KEY_PROPOSAL = "proposal" // $NON-NLS
// Using a custom saver instead of Parcelize, to avoid adding an Android-specific API to
// the ZecSend class
@ -27,7 +29,11 @@ internal val ZecSend.Companion.Saver
val address = runBlocking { WalletAddress.Unified.new(it[KEY_ADDRESS] as String) }
val amount = Zatoshi(it[KEY_AMOUNT] as Long)
val memo = Memo(it[KEY_MEMO] as String)
ZecSend(address, amount, memo)
val proposal =
it[KEY_PROPOSAL]?.let { data ->
Proposal.fromByteArray(data as ByteArray)
}
ZecSend(address, amount, memo, proposal)
}
}
)
@ -38,4 +44,7 @@ private fun ZecSend.toSaverMap() =
put(KEY_ADDRESS, destination.address)
put(KEY_AMOUNT, amount.value)
put(KEY_MEMO, memo.value)
proposal?.let {
put(KEY_PROPOSAL, it.toByteArray())
}
}

View File

@ -40,7 +40,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.MonetarySeparators
@ -88,7 +87,7 @@ private fun PreviewSendForm() {
sendStage = SendStage.Form,
onSendStageChange = {},
zecSend = null,
onZecSendChange = {},
onCreateZecSend = {},
focusManager = LocalFocusManager.current,
onBack = {},
onSettings = {},
@ -117,7 +116,8 @@ private fun PreviewSendSuccessful() {
ZecSend(
destination = runBlocking { WalletAddressFixture.sapling() },
amount = ZatoshiFixture.new(),
memo = MemoFixture.new()
memo = MemoFixture.new(),
proposal = null,
),
onDone = {}
)
@ -135,7 +135,8 @@ private fun PreviewSendFailure() {
ZecSend(
destination = runBlocking { WalletAddressFixture.sapling() },
amount = ZatoshiFixture.new(),
memo = MemoFixture.new()
memo = MemoFixture.new(),
proposal = null,
),
onDone = {},
reason = "Insufficient balance"
@ -154,7 +155,8 @@ private fun PreviewSendConfirmation() {
ZecSend(
destination = runBlocking { WalletAddressFixture.sapling() },
amount = ZatoshiFixture.new(),
memo = MemoFixture.new()
memo = MemoFixture.new(),
proposal = null,
),
onConfirmation = {}
)
@ -169,7 +171,7 @@ fun Send(
sendStage: SendStage,
onSendStageChange: (SendStage) -> Unit,
zecSend: ZecSend?,
onZecSendChange: (ZecSend) -> Unit,
onCreateZecSend: (ZecSend) -> Unit,
focusManager: FocusManager,
onBack: () -> Unit,
onSettings: () -> Unit,
@ -198,7 +200,7 @@ fun Send(
sendStage = sendStage,
onSendStageChange = onSendStageChange,
zecSend = zecSend,
onZecSendChange = onZecSendChange,
onCreateZecSend = onCreateZecSend,
recipientAddressState = recipientAddressState,
onRecipientAddressChange = onRecipientAddressChange,
amountState = amountState,
@ -259,7 +261,7 @@ private fun SendMainContent(
onBack: () -> Unit,
goBalances: () -> Unit,
zecSend: ZecSend?,
onZecSendChange: (ZecSend) -> Unit,
onCreateZecSend: (ZecSend) -> Unit,
sendStage: SendStage,
onSendStageChange: (SendStage) -> Unit,
onSendSubmit: (ZecSend) -> Unit,
@ -283,10 +285,7 @@ private fun SendMainContent(
setAmountState = setAmountState,
memoState = memoState,
setMemoState = setMemoState,
onCreateZecSend = {
onSendStageChange(SendStage.Confirmation)
onZecSendChange(it)
},
onCreateZecSend = onCreateZecSend,
focusManager = focusManager,
onQrScannerOpen = onQrScannerOpen,
goBalances = goBalances,
@ -334,7 +333,6 @@ private fun SendMainContent(
// TODO [#1257]: Send.Form TextFields not persisted on a configuration change when the underlying ViewPager is on the
// Balances page
// TODO [#1257]: https://github.com/Electric-Coin-Company/zashi-android/issues/1257
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "LongParameterList")
@Composable
private fun SendForm(
@ -436,7 +434,7 @@ private fun SendForm(
recipientAddressState.address.isNotEmpty() &&
amountState is AmountState.Valid &&
amountState.value.isNotBlank() &&
walletSnapshot.spendableBalance() >= (amountState.zatoshi + ZcashSdk.MINERS_FEE) &&
walletSnapshot.spendableBalance() >= amountState.zatoshi &&
// A valid memo is necessary only for non-transparent recipient
(recipientAddressState.type == AddressType.Transparent || memoState is MemoState.Correct)
@ -601,7 +599,7 @@ fun SendFormAmountTextField(
}
}
is AmountState.Valid -> {
if (walletSnapshot.spendableBalance() < (amountSate.zatoshi + ZcashSdk.MINERS_FEE)) {
if (walletSnapshot.spendableBalance() < amountSate.zatoshi) {
stringResource(id = R.string.send_amount_insufficient_balance)
} else {
null
@ -775,8 +773,13 @@ private fun SendConfirmation(
) {
Body(
stringResource(
R.string.send_confirmation_amount_and_address_format,
R.string.send_confirmation_amount_format,
zecSend.amount.toZecString(),
)
)
Body(
stringResource(
R.string.send_confirmation_address_format,
zecSend.destination.abbreviated()
)
)
@ -788,6 +791,19 @@ private fun SendConfirmation(
)
)
}
if (zecSend.proposal != null) {
// The not-null assertion operator is necessary here even if we check its nullability before
// due to: "Smart cast to 'Proposal' is impossible, because 'zecSend.proposal' is a public API
// property declared in different module
// See more details on the Kotlin forum
checkNotNull(zecSend.proposal)
Body(
stringResource(
R.string.send_confirmation_fee_format,
zecSend.proposal!!.totalFeeRequired().toZecString()
)
)
}
Spacer(
modifier =

View File

@ -19,8 +19,10 @@
<string name="send_create">Review</string>
<string name="send_fee">(Typical Fee &lt; <xliff:g id="fee_amount" example="0.001">%1$s</xliff:g>)</string>
<string name="send_confirmation_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
<string name="send_confirmation_amount_format" formatted="true">Send: <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC</string>
<string name="send_confirmation_address_format" formatted="true">To: <xliff:g id="address" example="zs1g7cqw … mvyzgm">%1$s</xliff:g>?</string>
<string name="send_confirmation_memo_format" formatted="true">Memo: <xliff:g id="memo" example="for Veronika">%1$s</xliff:g></string>
<string name="send_confirmation_fee_format" formatted="true">Fee: <xliff:g id="fee" example="0.0001">%1$s</xliff:g></string>
<string name="send_confirmation_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g><xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
<string name="send_confirmation_button">Press to send ZEC</string>