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