Iterate on the Send demo.

Allow input. Allow multiple sends. Handle errors. Demonstrate an improved user experience where sending is disabled at the appropriate times.
This commit is contained in:
Kevin Gorham 2019-11-23 18:07:28 -05:00
parent 93d4114848
commit 652e862d5c
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
5 changed files with 174 additions and 52 deletions

View File

@ -2,19 +2,18 @@ package cash.z.wallet.sdk.demoapp.demos.send
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.demoapp.App
import cash.z.wallet.sdk.demoapp.BaseDemoFragment
import cash.z.wallet.sdk.demoapp.R
import cash.z.wallet.sdk.demoapp.databinding.FragmentSendBinding
import cash.z.wallet.sdk.demoapp.util.SampleStorageBridge
import cash.z.wallet.sdk.entity.*
import cash.z.wallet.sdk.ext.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
private val config = App.instance.defaultConfig
@ -23,6 +22,36 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
private lateinit var synchronizer: Synchronizer
private lateinit var keyManager: SampleStorageBridge
private lateinit var amountInput: TextView
private lateinit var addressInput: TextView
//
// Observable properties (done without livedata or flows for simplicity)
//
private var availableBalance = -1L
set(value) {
field = value
onUpdateSendButton()
}
private var isSending = false
set(value) {
field = value
if (value) Twig.sprout("Sending") else Twig.clip("Sending")
onUpdateSendButton()
}
private var isSyncing = true
set(value) {
field = value
onUpdateSendButton()
}
//
// BaseDemoFragment overrides
//
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentSendBinding =
FragmentSendBinding.inflate(layoutInflater)
@ -32,13 +61,30 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
synchronizer = Synchronizer(App.instance, config.host, initializer.rustBackend)
}
// STARTING POINT
override fun onResetComplete() {
initSendUI()
initSendUi()
startSynchronizer()
monitorStatus()
monitorChanges()
}
private fun initSendUI() {
override fun onClear() {
synchronizer.stop()
initializer.clear()
}
//
// Private functions
//
private fun initSendUi() {
amountInput = binding.root.findViewById<TextView>(R.id.input_amount).apply {
text = config.sendAmount.toString()
}
addressInput = binding.root.findViewById<TextView>(R.id.input_address).apply {
text = config.toAddress
}
binding.buttonSend.setOnClickListener(::onSend)
}
@ -48,7 +94,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
}
}
private fun monitorStatus() {
private fun monitorChanges() {
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
synchronizer.balances.collectWith(lifecycleScope, ::onBalance)
@ -56,11 +102,12 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
private fun onStatus(status: Synchronizer.Status) {
binding.textStatus.text = "Status: $status"
}
override fun onClear() {
synchronizer.stop()
initializer.clear()
if (status == Synchronizer.Status.SYNCING) {
isSyncing = true
binding.textBalance.text = "Calculating balance..."
} else {
isSyncing = false
}
}
private fun onProgress(i: Int) {
@ -69,39 +116,69 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
else -> "Downloading blocks...$i%"
}
binding.textStatus.text = message
binding.textBalance.text = ""
}
private fun onBalance(balance: CompactBlockProcessor.WalletBalance) {
binding.textBalances.text = """
availableBalance = balance.available
binding.textBalance.text = """
Available balance: ${balance.available.convertZatoshiToZecString()}
Total balance: ${balance.total.convertZatoshiToZecString()}
""".trimIndent()
binding.buttonSend.isEnabled = balance.available > 0
binding.textStatus.text = "Synced!"
}
private fun onSend(unused: View) {
// TODO: add input fields to the UI. Possibly, including a scanner for the address input
isSending = true
val amount = amountInput.text.toString().toDouble().convertZecToZatoshi()
val toAddress = addressInput.text.toString()
synchronizer.sendToAddress(
keyManager.key,
0.0024.toZec().convertZecToZatoshi(),
config.toAddress,
amount,
toAddress,
"Demo App Funds"
).collectWith(lifecycleScope, ::onPendingTxUpdated)
}
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
val id = pendingTransaction?.id ?: -1
val message = when {
pendingTransaction == null -> "Transaction not found"
pendingTransaction.isMined() -> "Transaction Mined!"
pendingTransaction.isSubmitted() -> "Successfully submitted transaction!"
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction!"
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction!"
pendingTransaction.isCreated() -> "Transaction creation complete!"
pendingTransaction.isCreating() -> "Creating transaction!"
pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false }
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation..."
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false }
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false }
pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)"
pendingTransaction.isCreating() -> "Creating transaction!".also { onResetInfo() }
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
}
twig("PENDING TX: $message")
Toast.makeText(App.instance, message, Toast.LENGTH_SHORT).show()
twig("Pending TX Updated: $message")
binding.textInfo.apply {
text = "$text\n$message"
}
}
private fun onUpdateSendButton() {
with(binding.buttonSend) {
when {
isSending -> {
text = "➡ sending"
isEnabled = false
}
isSyncing -> {
text = "⌛ syncing"
isEnabled = false
}
availableBalance <= 0 -> isEnabled = false
else -> {
text = "send"
isEnabled = true
}
}
}
}
private fun onResetInfo() {
binding.textInfo.text = "Active Transaction:"
}
}

View File

@ -2,7 +2,6 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -15,14 +14,32 @@
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.2">
app:layout_constraintVertical_bias="0.05">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_block_height"
android:id="@+id/input_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="8"
android:hint="amount"
android:hint="zec amount"
android:inputType="number"
android:textSize="20sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/text_layout_amount"
app:layout_constraintTop_toBottomOf="@id/text_layout_amount">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="8"
android:hint="to address"
android:inputType="number"
android:textSize="20sp" />
</com.google.android.material.textfield.TextInputLayout>
@ -46,14 +63,30 @@
android:layout_marginTop="32dp"
android:text="Initializing wallet..."
app:layout_constraintStart_toStartOf="@id/text_layout_amount"
app:layout_constraintTop_toBottomOf="@id/text_layout_amount" />
app:layout_constraintTop_toBottomOf="@id/text_layout_address" />
<TextView
android:id="@+id/text_balances"
android:id="@+id/text_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@id/text_status"
app:layout_constraintTop_toBottomOf="@id/text_status"
android:text="Available balance: --\nTotal balance: --" />
app:layout_constraintTop_toBottomOf="@id/text_status" />
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="@id/text_balance"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_balance"
app:layout_constraintBottom_toBottomOf="parent"
>
<TextView
android:id="@+id/text_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingEnd="8dp"
android:paddingBottom="48dp"/>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,7 +1,6 @@
package cash.z.wallet.sdk
import android.content.Context
import androidx.paging.PagedList
import cash.z.wallet.sdk.Synchronizer.Status.*
import cash.z.wallet.sdk.block.CompactBlockDbStore
import cash.z.wallet.sdk.block.CompactBlockDownloader
@ -254,16 +253,19 @@ class SdkSynchronizer internal constructor(
// TODO: this would be the place to clear out any stale pending transactions. Remove filter
// logic and then delete any pending transaction with sufficient confirmations (all in one
// db transaction).
manager.getAll().first().filter { !it.isMined() }.forEach { pendingTx ->
twig("checking for updates on pendingTx id: ${pendingTx.id}")
pendingTx.rawTransactionId?.let { rawId ->
ledger.findMinedHeight(rawId)?.let { minedHeight ->
twig("found matching transaction for pending transaction with id" +
" ${pendingTx.id} mined at height ${minedHeight}!")
manager.applyMinedHeight(pendingTx, minedHeight)
manager.getAll().first().filter { it.isSubmitSuccess() && !it.isMined() }
.forEach { pendingTx ->
twig("checking for updates on pendingTx id: ${pendingTx.id}")
pendingTx.rawTransactionId?.let { rawId ->
ledger.findMinedHeight(rawId)?.let { minedHeight ->
twig(
"found matching transaction for pending transaction with id" +
" ${pendingTx.id} mined at height ${minedHeight}!"
)
manager.applyMinedHeight(pendingTx, minedHeight)
}
}
}
}
}
@ -287,13 +289,15 @@ class SdkSynchronizer internal constructor(
manager.initSpend(zatoshi, toAddress, memo, fromAccountIndex).let { placeHolderTx ->
emit(placeHolderTx)
manager.encode(spendingKey, placeHolderTx).let { encodedTx ->
manager.submit(encodedTx)
if (!encodedTx.isFailedEncoding() && !encodedTx.isCancelled()) {
manager.submit(encodedTx)
}
}
}
}.flatMapLatest {
twig("Monitoring pending transaction for updates...")
twig("Monitoring pending transaction (id: ${it.id}) for updates...")
manager.monitorById(it.id)
}
}.distinctUntilChanged()
}

View File

@ -141,6 +141,15 @@ inline fun Double?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): B
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(decimals, ZEC_FORMATTER.roundingMode)
}
/**
* Format a Double Zec value as a Long Zatoshi value, by first converting to Zec with the given
* precision.
* Start with Zec -> End with Zatoshi.
*/
inline fun Double?.convertZecToZatoshi(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): Long {
return this.toZec(decimals).convertZecToZatoshi()
}
/**
* Format a BigDecimal Zec value as a BigDecimal Zec value, right-padded to the given number of fraction digits.
* Start with Zec -> End with Zec.

View File

@ -1,8 +1,6 @@
package cash.z.wallet.sdk.transaction
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.room.Room
import androidx.room.RoomDatabase
import cash.z.wallet.sdk.db.PendingTransactionDao
@ -15,6 +13,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.lang.IllegalStateException
import kotlin.math.max
/**
@ -134,14 +133,14 @@ class PersistentTransactionManager(
}
override suspend fun submit(pendingTx: PendingTransaction): PendingTransaction = withContext(Dispatchers.IO) {
var tx1 = pendingTransactionDao { findById(pendingTx.id) }
if(tx1 == null) twig("unable to find transaction for id: ${pendingTx.id}")
var tx = tx1!!
// reload the tx to check for cancellation
var storedTx = pendingTransactionDao { findById(pendingTx.id) } ?: throw IllegalStateException("Error while submitting transaction. No pending transaction found that matches the one being submitted. Verify that the transaction still exists among the set of pending transactions.")
var tx = storedTx
try {
// do nothing when cancelled
if (!tx.isCancelled()) {
twig("submitting transaction to lightwalletd - memo: ${tx.memo} amount: ${tx.value}")
val response = service.submitTransaction(tx.raw!!)
val response = service.submitTransaction(tx.raw)
val error = response.errorCode < 0
twig("${if (error) "FAILURE! " else "SUCCESS!"} submit transaction completed with response: ${response.errorCode}: ${response.errorMessage}")
tx = tx.copy(