diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/send/SendFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/send/SendFragment.kt index 95cf112a..e694232c 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/send/SendFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/send/SendFragment.kt @@ -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() { private val config = App.instance.defaultConfig @@ -23,6 +22,36 @@ class SendFragment : BaseDemoFragment() { 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() { 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(R.id.input_amount).apply { + text = config.sendAmount.toString() + } + addressInput = binding.root.findViewById(R.id.input_address).apply { + text = config.toAddress + } binding.buttonSend.setOnClickListener(::onSend) } @@ -48,7 +94,7 @@ class SendFragment : BaseDemoFragment() { } } - 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() { 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() { 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:" + } + } diff --git a/samples/demo-app/app/src/main/res/layout/fragment_send.xml b/samples/demo-app/app/src/main/res/layout/fragment_send.xml index 56470973..91942426 100644 --- a/samples/demo-app/app/src/main/res/layout/fragment_send.xml +++ b/samples/demo-app/app/src/main/res/layout/fragment_send.xml @@ -2,7 +2,6 @@ @@ -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"> + + + + + @@ -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" /> + app:layout_constraintTop_toBottomOf="@id/text_status" /> + + + + \ No newline at end of file diff --git a/src/main/java/cash/z/wallet/sdk/SdkSynchronizer.kt b/src/main/java/cash/z/wallet/sdk/SdkSynchronizer.kt index bf497251..b0db06ce 100644 --- a/src/main/java/cash/z/wallet/sdk/SdkSynchronizer.kt +++ b/src/main/java/cash/z/wallet/sdk/SdkSynchronizer.kt @@ -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() } diff --git a/src/main/java/cash/z/wallet/sdk/ext/CurrencyFormatter.kt b/src/main/java/cash/z/wallet/sdk/ext/CurrencyFormatter.kt index 69b9bf12..eb219461 100644 --- a/src/main/java/cash/z/wallet/sdk/ext/CurrencyFormatter.kt +++ b/src/main/java/cash/z/wallet/sdk/ext/CurrencyFormatter.kt @@ -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. diff --git a/src/main/java/cash/z/wallet/sdk/transaction/PersistentTransactionManager.kt b/src/main/java/cash/z/wallet/sdk/transaction/PersistentTransactionManager.kt index 192b9181..e59b2334 100644 --- a/src/main/java/cash/z/wallet/sdk/transaction/PersistentTransactionManager.kt +++ b/src/main/java/cash/z/wallet/sdk/transaction/PersistentTransactionManager.kt @@ -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(