From 508e6d3da999456abd0dd1ddb7f11424d7df407d Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 26 Feb 2020 02:43:27 -0500 Subject: [PATCH] Demo app improvements. including: mainnet support, BIP39 support and using seed words instead of string-based seeds, several convenience functions like copyToClipboard, simplified the developer experience for using the FAB, corrected errors in port numbers, streamlined several demos, trim user input, better messaging using new processorInfo flow. --- samples/demo-app/app/build.gradle | 23 +++++- .../java/cash/z/wallet/sdk/demoapp/App.kt | 1 - .../z/wallet/sdk/demoapp/BaseDemoFragment.kt | 56 ++++++++++++- .../cash/z/wallet/sdk/demoapp/MainActivity.kt | 13 ++- .../demos/getaddress/GetAddressFragment.kt | 21 ++--- .../demos/getblock/GetBlockFragment.kt | 3 +- .../getblockrange/GetBlockRangeFragment.kt | 3 +- .../GetLatestHeightFragment.kt | 8 +- .../getprivatekey/GetPrivateKeyFragment.kt | 2 +- .../ListTransactionsFragment.kt | 41 ++++++++-- .../sdk/demoapp/demos/send/SendFragment.kt | 41 ++++++---- .../z/wallet/sdk/demoapp/util/DemoConfig.kt | 19 ----- .../sdk/demoapp/util/SimpleMnemonics.kt | 82 +++++++++++++++++++ .../res/layout/fragment_list_transactions.xml | 27 +++--- .../app/src/main/res/values/strings.xml | 1 - .../cash/z/wallet/sdk/demoapp/DemoConfig.kt | 22 +++++ .../src/zcashmainnet/res/values/strings.xml | 4 + samples/demo-app/build.gradle | 5 +- .../wallet/sdk/block/CompactBlockProcessor.kt | 38 +++++++++ 19 files changed, 328 insertions(+), 82 deletions(-) delete mode 100644 samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/DemoConfig.kt create mode 100644 samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/SimpleMnemonics.kt create mode 100644 samples/demo-app/app/src/zcashmainnet/java/cash/z/wallet/sdk/demoapp/DemoConfig.kt create mode 100644 samples/demo-app/app/src/zcashmainnet/res/values/strings.xml diff --git a/samples/demo-app/app/build.gradle b/samples/demo-app/app/build.gradle index ad7c0d73..19b1a6cf 100644 --- a/samples/demo-app/app/build.gradle +++ b/samples/demo-app/app/build.gradle @@ -15,7 +15,21 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - missingDimensionStrategy "network", "zcashtestnet" + } + flavorDimensions 'network' + productFlavors { + // would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test" + zcashtestnet { + dimension 'network' + applicationId 'cash.z.wallet.sdk.demoapp.testnet' + matchingFallbacks = ['zcashtestnet', 'debug'] + } + + zcashmainnet { + dimension 'network' + applicationId 'cash.z.wallet.sdk.demoapp.mainnet' + matchingFallbacks = ['zcashmainnet', 'release'] + } } buildTypes { release { @@ -36,7 +50,12 @@ dependencies { // SDK implementation project(path: ':sdk') // implementation "cash.z.android.wallet:zcash-android-core:$sdk_version@aar" -// implementation "cash.z.android.wallet:zcash-kotlin-bip39:$sdk_version@aar" + + // sample mnemonic plugin + implementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.0' + implementation 'com.madgag.spongycastle:core:1.58.0.0' + implementation 'io.github.novacrypto:BIP39:2019.01.27' + implementation 'io.github.novacrypto:securestring:2019.01.27' // SDK: grpc implementation "io.grpc:grpc-okhttp:1.21.0" diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/App.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/App.kt index 551ea494..636c23bd 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/App.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/App.kt @@ -1,7 +1,6 @@ package cash.z.wallet.sdk.demoapp import android.app.Application -import cash.z.wallet.sdk.demoapp.util.DemoConfig class App : Application() { diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/BaseDemoFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/BaseDemoFragment.kt index 3b1559b2..5cbf55d9 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/BaseDemoFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/BaseDemoFragment.kt @@ -1,14 +1,19 @@ package cash.z.wallet.sdk.demoapp +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding import cash.z.wallet.sdk.ext.TroubleshootingTwig import cash.z.wallet.sdk.ext.Twig +import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext @@ -32,6 +37,7 @@ abstract class BaseDemoFragment : Fragment() { override fun onResume() { super.onResume() + registerActionButtonListener() // just a quick way of enforcing the following for each demo: // - wait until the fragment is created, then run `initInBackground` on a background thread // - wait until init is finished @@ -59,9 +65,58 @@ abstract class BaseDemoFragment : Fragment() { override fun onPause() { super.onPause() + unregisterActionButtonListener() onClear() } + private fun registerActionButtonListener() { + (activity as? MainActivity)?.fabListener = this + } + + private fun unregisterActionButtonListener() { + (activity as? MainActivity)?.apply { + if (fabListener === this@BaseDemoFragment) fabListener = null + } + } + + /** + * Callback to run whenever the fragment is paused. The intention is to clear out each demo + * cleanly so that they are always a repeatable experience. + */ + open fun onClear() {} + + /** + * Callback that gets invoked on the visible fragment whenever the floating action button is + * tapped. This provides a convenient placeholder for the developer to extend the + * behavior for a demo, for instance by copying the address to the clipboard, whenever the FAB + * is tapped on the address screen. + */ + open fun onActionButtonClicked() { + // Show a message so that it's easy for developers to find how to replace this behavior for + // each fragment. Simply override this [onActionButtonClicked] callback to add behavior to a + // demo. In other words, this function probably doesn't need to change because desired + // behavior should go in the child fragment, which overrides this. + Snackbar.make(view!!, "Replace with your own action", Snackbar.LENGTH_LONG) + .setAction("Action") { /* auto-close */ }.show() + } + + /** + * Convenience function to the given text to the clipboard. + */ + open fun copyToClipboard(text: String) { + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)?.let { cm -> + cm.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text)) + } + toast("Copied to clipboard!") + } + + /** + * Convenience function to show a toast in the main activity. + */ + fun toast(message: String) { + Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() + } + /** * Inflate the ViewBinding. Unfortunately, the `inflate` function is not part of the ViewBinding * interface so the base class cannot take care of this behavior without some help. @@ -69,5 +124,4 @@ abstract class BaseDemoFragment : Fragment() { abstract fun inflateBinding(layoutInflater: LayoutInflater): T abstract fun resetInBackground() abstract fun onResetComplete() - abstract fun onClear() } \ No newline at end of file diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/MainActivity.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/MainActivity.kt index 8969f4b2..38fc650d 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/MainActivity.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/MainActivity.kt @@ -2,6 +2,7 @@ package cash.z.wallet.sdk.demoapp import android.os.Bundle import android.view.Menu +import android.view.View import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import com.google.android.material.navigation.NavigationView @@ -13,11 +14,12 @@ import androidx.navigation.ui.setupWithNavController import androidx.drawerlayout.widget.DrawerLayout import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar -import cash.z.wallet.sdk.demoapp.util.DemoConfig +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding class MainActivity : AppCompatActivity() { - private lateinit var appBarConfiguration: AppBarConfiguration + var fabListener: BaseDemoFragment? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -27,8 +29,7 @@ class MainActivity : AppCompatActivity() { val fab: FloatingActionButton = findViewById(R.id.fab) fab.setOnClickListener { view -> - Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) - .setAction("Action", null).show() + onFabClicked(view) } val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout) val navView: NavigationView = findViewById(R.id.nav_view) @@ -46,6 +47,10 @@ class MainActivity : AppCompatActivity() { navView.setupWithNavController(navController) } + private fun onFabClicked(view: View) { + fabListener?.onActionButtonClicked() + } + override fun onCreateOptionsMenu(menu: Menu): Boolean { // Inflate the menu; this adds items to the action bar if it is present. menuInflater.inflate(R.menu.main, menu) diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getaddress/GetAddressFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getaddress/GetAddressFragment.kt index fadb6514..941bbee9 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getaddress/GetAddressFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getaddress/GetAddressFragment.kt @@ -8,27 +8,24 @@ import cash.z.wallet.sdk.demoapp.databinding.FragmentGetAddressBinding class GetAddressFragment : BaseDemoFragment() { - private val config = App.instance.defaultConfig - private var seed: ByteArray = config.seed - private val initializer: Initializer = Initializer(App.instance, host = config.host, port = config.port) - private val birthday = config.newWalletBirthday() + private var seed: ByteArray = App.instance.defaultConfig.seed + private val initializer: Initializer = Initializer(App.instance) + + private lateinit var address: String override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetAddressBinding = FragmentGetAddressBinding.inflate(layoutInflater) override fun resetInBackground() { - /** - * Create and initialize the wallet. Initialization will return the private keys but for the - * purposes of this demo we don't need them. - */ - initializer.new(seed, birthday) + address = initializer.deriveAddress(seed) } override fun onResetComplete() { - binding.textInfo.text = initializer.rustBackend.getAddress() + binding.textInfo.text = address } - override fun onClear() { - initializer.clear() + override fun onActionButtonClicked() { + copyToClipboard(address) } + } \ No newline at end of file diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblock/GetBlockFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblock/GetBlockFragment.kt index 6f528916..b7ef4452 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblock/GetBlockFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblock/GetBlockFragment.kt @@ -10,6 +10,7 @@ import cash.z.wallet.sdk.service.LightWalletService class GetBlockFragment : BaseDemoFragment() { private val host = App.instance.defaultConfig.host + private val port = App.instance.defaultConfig.port private lateinit var lightwalletService: LightWalletService @@ -17,7 +18,7 @@ class GetBlockFragment : BaseDemoFragment() { FragmentGetBlockBinding.inflate(layoutInflater) override fun resetInBackground() { - lightwalletService = LightWalletGrpcService(App.instance, host) + lightwalletService = LightWalletGrpcService(App.instance, host, port) } override fun onResetComplete() { diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt index 22f4cbc4..7223db43 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt @@ -11,6 +11,7 @@ import cash.z.wallet.sdk.service.LightWalletService class GetBlockRangeFragment : BaseDemoFragment() { private val host = App.instance.defaultConfig.host + private val port = App.instance.defaultConfig.port private lateinit var lightwalletService: LightWalletService @@ -18,7 +19,7 @@ class GetBlockRangeFragment : BaseDemoFragment() { FragmentGetBlockRangeBinding.inflate(layoutInflater) override fun resetInBackground() { - lightwalletService = LightWalletGrpcService(App.instance, host) + lightwalletService = LightWalletGrpcService(App.instance, host, port) } override fun onResetComplete() { diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt index 9619b7a9..4718d1ed 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt @@ -10,6 +10,7 @@ import cash.z.wallet.sdk.service.LightWalletService class GetLatestHeightFragment : BaseDemoFragment() { private val host = App.instance.defaultConfig.host + private val port = App.instance.defaultConfig.port private lateinit var lightwalletService: LightWalletService @@ -17,7 +18,7 @@ class GetLatestHeightFragment : BaseDemoFragment FragmentGetLatestHeightBinding.inflate(layoutInflater) override fun resetInBackground() { - lightwalletService = LightWalletGrpcService(App.instance, host) + lightwalletService = LightWalletGrpcService(App.instance, host, port) } override fun onResetComplete() { @@ -27,4 +28,9 @@ class GetLatestHeightFragment : BaseDemoFragment override fun onClear() { lightwalletService.shutdown() } + + override fun onActionButtonClicked() { + toast("Refreshed!") + onResetComplete() + } } \ No newline at end of file diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt index 890edb7f..4a595730 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt @@ -26,7 +26,7 @@ class GetPrivateKeyFragment : BaseDemoFragment() { spendingKeys = initializer.new(seed, birthday) /* - * Viewing keys can be derived from a seed or from spending keys. + * Alternatively, viewing keys can also be derived directly from a seed or spending keys. */ viewingKeys = initializer.deriveViewingKeys(seed) diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt index 8e1fd904..2d0a3455 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt @@ -7,6 +7,7 @@ import androidx.paging.PagedList import androidx.recyclerview.widget.LinearLayoutManager 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.databinding.FragmentListTransactionsBinding @@ -23,9 +24,13 @@ import kotlinx.coroutines.launch class ListTransactionsFragment : BaseDemoFragment() { private val config = App.instance.defaultConfig private val initializer = Initializer(App.instance, host = config.host, port = config.port) - private val birthday = config.newWalletBirthday() + private val birthday = config.loadBirthday() private lateinit var synchronizer: Synchronizer private lateinit var adapter: TransactionAdapter + private lateinit var address: String + private var status: Synchronizer.Status? = null + + private val isSynced get() = status == Synchronizer.Status.SYNCED override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListTransactionsBinding = FragmentListTransactionsBinding.inflate(layoutInflater) @@ -51,6 +56,7 @@ class ListTransactionsFragment : BaseDemoFragment "Scanning blocks..." - else -> "Downloading blocks...$i%" - } - binding.textInfo.text = message + if (i < 100) binding.textInfo.text = "Downloading blocks...$i%" } private fun onStatus(status: Synchronizer.Status) { + this.status = status binding.textStatus.text = "Status: $status" - if (status == Synchronizer.Status.SYNCED) onSyncComplete() + if (isSynced) onSyncComplete() } private fun onSyncComplete() { @@ -87,5 +95,24 @@ class ListTransactionsFragment : BaseDemoFragment) { twig("got a new paged list of transactions") adapter.submitList(transactions) + + // show message when there are no transactions + if (isSynced) { + binding.textInfo.apply { + if (transactions.isEmpty()) { + visibility = View.VISIBLE + text = + "No transactions found. Try to either change the seed words in the" + + " DemoConfig.kt file or send funds to this address (tap the FAB to copy it):\n\n $address" + } else { + visibility = View.INVISIBLE + text = "" + } + } + } + } + + override fun onActionButtonClicked() { + if (::address.isInitialized) copyToClipboard(address) } } 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 6d1e26c2..31d8b257 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 @@ -18,7 +18,7 @@ import cash.z.wallet.sdk.ext.* class SendFragment : BaseDemoFragment() { private val config = App.instance.defaultConfig private val initializer = Initializer(App.instance, host = config.host, port = config.port) - private val birthday = config.newWalletBirthday() + private val birthday = config.loadBirthday() private lateinit var synchronizer: Synchronizer private lateinit var keyManager: SampleStorageBridge @@ -31,7 +31,7 @@ class SendFragment : BaseDemoFragment() { // Observable properties (done without livedata or flows for simplicity) // - private var availableBalance = -1L + private var balance = CompactBlockProcessor.WalletBalance() set(value) { field = value onUpdateSendButton() @@ -98,40 +98,47 @@ class SendFragment : BaseDemoFragment() { private fun monitorChanges() { synchronizer.status.collectWith(lifecycleScope, ::onStatus) synchronizer.progress.collectWith(lifecycleScope, ::onProgress) + synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated) synchronizer.balances.collectWith(lifecycleScope, ::onBalance) } private fun onStatus(status: Synchronizer.Status) { binding.textStatus.text = "Status: $status" - if (status != Synchronizer.Status.SYNCED) { - isSyncing = true + isSyncing = status != Synchronizer.Status.SYNCED + if (status == Synchronizer.Status.SCANNING) { binding.textBalance.text = "Calculating balance..." } else { - isSyncing = false + if (!isSyncing) onBalance(balance) } } private fun onProgress(i: Int) { - val message = when (i) { - 100 -> "Scanning blocks..." - else -> "Downloading blocks...$i%" + if (i < 100) { + binding.textStatus.text = "Downloading blocks...$i%" + binding.textBalance.visibility = View.INVISIBLE + } else { + binding.textBalance.visibility = View.VISIBLE } - binding.textStatus.text = message - binding.textBalance.text = "" + } + + private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) { + if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%" } private fun onBalance(balance: CompactBlockProcessor.WalletBalance) { - availableBalance = balance.availableZatoshi - binding.textBalance.text = """ - Available balance: ${balance.availableZatoshi.convertZatoshiToZecString()} - Total balance: ${balance.totalZatoshi.convertZatoshiToZecString()} - """.trimIndent() + this.balance = balance + if (!isSyncing) { + binding.textBalance.text = """ + Available balance: ${balance.availableZatoshi.convertZatoshiToZecString(12)} + Total balance: ${balance.totalZatoshi.convertZatoshiToZecString(12)} + """.trimIndent() + } } private fun onSend(unused: View) { isSending = true val amount = amountInput.text.toString().toDouble().convertZecToZatoshi() - val toAddress = addressInput.text.toString() + val toAddress = addressInput.text.toString().trim() synchronizer.sendToAddress( keyManager.key, amount, @@ -169,7 +176,7 @@ class SendFragment : BaseDemoFragment() { text = "⌛ syncing" isEnabled = false } - availableBalance <= 0 -> isEnabled = false + balance.availableZatoshi <= 0 -> isEnabled = false else -> { text = "send" isEnabled = true diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/DemoConfig.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/DemoConfig.kt deleted file mode 100644 index 3e8a86db..00000000 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/DemoConfig.kt +++ /dev/null @@ -1,19 +0,0 @@ -package cash.z.wallet.sdk.demoapp.util - -import cash.z.wallet.sdk.Initializer -import cash.z.wallet.sdk.demoapp.App - -data class DemoConfig( - val host: String = "34.68.177.238",//"192.168.1.134",// - val port: Int = 9067, - val birthdayHeight: Int = 620_000,//523_240, - val network: ZcashNetwork = ZcashNetwork.TEST_NET, - val seed: ByteArray = "testreferencealice____placeholder".toByteArray(), - val toAddress: String = "ztestsapling1fg82ar8y8whjfd52l0xcq0w3n7nn7cask2scp9rp27njeurr72ychvud57s9tu90fdqgwdt07lg", - val sendAmount: Double = 0.0024 -) { - fun newWalletBirthday() = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance) - fun loadBirthday() = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance, birthdayHeight) -} - -enum class ZcashNetwork { MAIN_NET, TEST_NET } \ No newline at end of file diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/SimpleMnemonics.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/SimpleMnemonics.kt new file mode 100644 index 00000000..4172826e --- /dev/null +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/SimpleMnemonics.kt @@ -0,0 +1,82 @@ +package cash.z.wallet.sdk.demoapp.util + +import cash.z.android.plugin.MnemonicPlugin +import io.github.novacrypto.bip39.MnemonicGenerator +import io.github.novacrypto.bip39.SeedCalculator +import io.github.novacrypto.bip39.Words +import io.github.novacrypto.bip39.wordlists.English +import java.security.SecureRandom + +class SimpleMnemonics : MnemonicPlugin { + + override fun nextEntropy(): ByteArray { + return ByteArray(Words.TWENTY_FOUR.byteLength()).apply { + SecureRandom().nextBytes(this) + } + } + + override fun nextMnemonic(): CharArray { + return nextMnemonic(nextEntropy()) + } + + override fun nextMnemonic(entropy: ByteArray): CharArray { + return StringBuilder().let { builder -> + MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c -> + builder.append(c) + } + builder.toString().toCharArray() + } + } + + override fun nextMnemonicList(): List { + return nextMnemonicList(nextEntropy()) + } + + override fun nextMnemonicList(entropy: ByteArray): List { + return WordListBuilder().let { builder -> + MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c -> + builder.append(c) + } + builder.wordList + } + } + + override fun toSeed(mnemonic: CharArray): ByteArray { + return SeedCalculator().calculateSeed(String(mnemonic), "") + } + + override fun toWordList(mnemonic: CharArray): List { + val wordList = mutableListOf() + var cursor = 0 + repeat(mnemonic.size) { i -> + val isSpace = mnemonic[i] == ' ' + if (isSpace || i == (mnemonic.size - 1)) { + val wordSize = i - cursor + if (isSpace) 0 else 1 + wordList.add(CharArray(wordSize).apply { + repeat(wordSize) { + this[it] = mnemonic[cursor + it] + } + }) + cursor = i + 1 + } + } + return wordList + } + + class WordListBuilder { + val wordList = mutableListOf() + fun append(c: CharSequence) { + if (c[0] != English.INSTANCE.space) addWord(c) + } + + private fun addWord(c: CharSequence) { + c.length.let { size -> + val word = CharArray(size) + repeat(size) { + word[it] = c[it] + } + wordList.add(word) + } + } + } +} \ No newline at end of file diff --git a/samples/demo-app/app/src/main/res/layout/fragment_list_transactions.xml b/samples/demo-app/app/src/main/res/layout/fragment_list_transactions.xml index 2e5e31a1..c95728a6 100644 --- a/samples/demo-app/app/src/main/res/layout/fragment_list_transactions.xml +++ b/samples/demo-app/app/src/main/res/layout/fragment_list_transactions.xml @@ -2,9 +2,16 @@ + android:layout_height="match_parent"> + + + app:layout_constraintVertical_bias="0.2" /> + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@id/text_status" /> Demo App Open navigation drawer Close navigation drawer - Android SDK Demo v1.0.0-alpha02 Navigation header Settings diff --git a/samples/demo-app/app/src/zcashmainnet/java/cash/z/wallet/sdk/demoapp/DemoConfig.kt b/samples/demo-app/app/src/zcashmainnet/java/cash/z/wallet/sdk/demoapp/DemoConfig.kt new file mode 100644 index 00000000..16338f5e --- /dev/null +++ b/samples/demo-app/app/src/zcashmainnet/java/cash/z/wallet/sdk/demoapp/DemoConfig.kt @@ -0,0 +1,22 @@ +package cash.z.wallet.sdk.demoapp + +import cash.z.wallet.sdk.Initializer +import cash.z.wallet.sdk.demoapp.util.SimpleMnemonics + +data class DemoConfig( + val host: String = "lightwalletd.z.cash", + val port: Int = 9067, + val birthdayHeight: Int = 735_000, + val sendAmount: Double = 0.0018, + + // corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx + val seedWords: String = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame", + + // corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top + val toAddress: String = "zs1lcdmue7rewgvzh3jd09sfvwq3sumu6hkhpk53q94kcneuffjkdg9e3tyxrugkmpza5c3c5e6eqh" +) { + val seed: ByteArray get() = SimpleMnemonics().toSeed(seedWords.toCharArray()) + + fun newWalletBirthday() = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance) + fun loadBirthday(height: Int = birthdayHeight) = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance, height) +} \ No newline at end of file diff --git a/samples/demo-app/app/src/zcashmainnet/res/values/strings.xml b/samples/demo-app/app/src/zcashmainnet/res/values/strings.xml new file mode 100644 index 00000000..9199fb1e --- /dev/null +++ b/samples/demo-app/app/src/zcashmainnet/res/values/strings.xml @@ -0,0 +1,4 @@ + + Demo App - Mainnet + Android SDK Demo : MAINNET + diff --git a/samples/demo-app/build.gradle b/samples/demo-app/build.gradle index 919f1e82..c824d9d4 100644 --- a/samples/demo-app/build.gradle +++ b/samples/demo-app/build.gradle @@ -20,12 +20,9 @@ buildscript { allprojects { repositories { -// mavenLocal() -// flatDir { -// dirs 'libs' -// } google() jcenter() + maven { url 'https://jitpack.io' } } } diff --git a/src/main/java/cash/z/wallet/sdk/block/CompactBlockProcessor.kt b/src/main/java/cash/z/wallet/sdk/block/CompactBlockProcessor.kt index 5472811f..8d06e206 100644 --- a/src/main/java/cash/z/wallet/sdk/block/CompactBlockProcessor.kt +++ b/src/main/java/cash/z/wallet/sdk/block/CompactBlockProcessor.kt @@ -402,10 +402,48 @@ class CompactBlockProcessor( val lastDownloadRange: IntRange = 0..-1, // empty range val lastScanRange: IntRange = 0..-1 // empty range ) { + + /** + * Returns false when all values match their defaults. + */ val hasData get() = networkBlockHeight != -1 || lastScannedHeight != -1 || lastDownloadedHeight != -1 || lastDownloadRange != 0..-1 || lastScanRange != 0..-1 + + /** + * Returns true when there are more than zero blocks remaining to download. + */ + val isDownloading: Boolean get() = !lastDownloadRange.isEmpty() + && lastDownloadedHeight < lastDownloadRange.last + + /** + * Returns true when downloading has completed and there are more than zero blocks remaining + * to be scanned. + */ + val isScanning: Boolean get() = !isDownloading + && !lastScanRange.isEmpty() + && lastScannedHeight < lastScanRange.last + + /** + * The amount of scan progress from 0 to 100. + */ + val scanProgress get() = when { + lastScannedHeight <= -1 -> 0 + lastScanRange.isEmpty() -> 100 + lastScannedHeight >= lastScanRange.last -> 100 + else -> { + // when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets + val blocksScanned = (lastScannedHeight - lastScanRange.first + 1).coerceAtLeast(0) + // we scan the range inclusively so 100..100 is one block to scan, thus the offset + val numberOfBlocks = lastScanRange.last - lastScanRange.first + 1 + // take the percentage then convert and round + ((blocksScanned.toFloat() / numberOfBlocks) * 100.0f).let { percent -> + percent.coerceAtMost(100.0f).roundToInt() + } + } + } + } } \ No newline at end of file