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:
parent
93d4114848
commit
652e862d5c
|
@ -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:"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue