Revamped demo app.

- Add logging
- Add service to mainactivity
- Use new SDK derivation tool
- Add prev/next buttons to block demo
- Add helpful stats to block range demo
- Add instructions to home screen and stop clearing after each demo
- Improve transaction list display
- Revamped every demo
- Added HTML to displays
- Added helpful extension functions
- Updated dependencies
This commit is contained in:
Kevin Gorham 2020-09-11 03:16:46 -04:00
parent 09f08bdd04
commit 3e6355b0c7
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
26 changed files with 547 additions and 300 deletions

View File

@ -49,8 +49,8 @@ android {
dependencies {
// SDK
zcashmainnetImplementation 'cash.z.ecc.android:sdk-mainnet:1.1.0-beta04'
zcashtestnetImplementation 'cash.z.ecc.android:sdk-testnet:1.1.0-beta04'
zcashmainnetImplementation 'cash.z.ecc.android:sdk-mainnet:1.1.0-beta05'
zcashtestnetImplementation 'cash.z.ecc.android:sdk-testnet:1.1.0-beta05'
// sample mnemonic plugin
implementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.1'
@ -68,20 +68,20 @@ dependencies {
implementation 'com.google.guava:guava:27.0.1-android'
kapt 'androidx.room:room-compiler:2.2.5'
// SDK: Other
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
// Android
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.0-alpha06'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha06' // provides lifecycleScope!
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.0-alpha07'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07' // provides lifecycleScope!
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
implementation "com.google.android.material:material:1.3.0-alpha02"
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

View File

@ -1,6 +1,8 @@
package cash.z.ecc.android.sdk.demoapp
import android.app.Application
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
class App : Application() {
@ -9,6 +11,7 @@ class App : Application() {
override fun onCreate() {
instance = this
super.onCreate()
Twig.plant(TroubleshootingTwig())
}
companion object {

View File

@ -11,21 +11,24 @@ import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import com.google.android.material.snackbar.Snackbar
abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
/**
* Since the lightwalletservice is not a component that apps typically use, directly, we provide
* this from one place. Everything that can be done with the service can/should be done with the
* synchronizer because it wraps the service.
*/
val lightwalletService get() = mainActivity()?.lightwalletService
// contains view information provided by the user
val sharedViewModel: SharedViewModel by activityViewModels()
lateinit var binding: T
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Twig.plant(TroubleshootingTwig())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -74,11 +77,11 @@ abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
/**
* Convenience function to the given text to the clipboard.
*/
open fun copyToClipboard(text: String) {
open fun copyToClipboard(text: String, description: String = "Copied to clipboard!") {
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)?.let { cm ->
cm.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text))
}
toast("Copied to clipboard!")
toast(description)
}
/**

View File

@ -17,6 +17,8 @@ import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView
@ -27,6 +29,15 @@ class MainActivity : AppCompatActivity(), ClipboardManager.OnPrimaryClipChangedL
private var clipboardListener: ((String?) -> Unit)? = null
var fabListener: BaseDemoFragment<out ViewBinding>? = null
/**
* The service to use for all demos that interact directly with the service. Since gRPC channels
* are expensive to recreate, we set this up once per demo. A real app would hardly ever use
* this object because it would utilize the synchronizer, instead, which exposes APIs that
* automatically sync with the server.
*/
var lightwalletService: LightWalletService? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
@ -54,12 +65,15 @@ class MainActivity : AppCompatActivity(), ClipboardManager.OnPrimaryClipChangedL
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
drawerLayout.addDrawerListener(this)
initService()
}
private fun onFabClicked(view: View) {
fabListener?.onActionButtonClicked()
override fun onDestroy() {
super.onDestroy()
lightwalletService?.shutdown()
}
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)
@ -82,6 +96,24 @@ class MainActivity : AppCompatActivity(), ClipboardManager.OnPrimaryClipChangedL
}
//
// Private functions
//
private fun initService() {
if (lightwalletService != null) {
lightwalletService?.shutdown()
}
with(App.instance.defaultConfig) {
lightwalletService = LightWalletGrpcService(App.instance, host, port)
}
}
private fun onFabClicked(view: View) {
fabListener?.onActionButtonClicked()
}
//
// Helpers
//

View File

@ -4,10 +4,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.demoapp.App
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding
import cash.z.ecc.android.sdk.tool.DerivationTool
/**
* Displays the address associated with the seed defined by the default config. To modify the seed
@ -15,7 +14,6 @@ import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding
*/
class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
private lateinit var initializer: Initializer
private lateinit var viewingKey: String
private lateinit var seed: ByteArray
@ -23,7 +21,7 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
* Initialize the required values that would normally live outside the demo but are repeated
* here for completeness so that each demo file can serve as a standalone example.
*/
fun setup() {
private fun setup() {
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
var seedPhrase = sharedViewModel.seedPhrase.value
@ -31,17 +29,16 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
// have the seed stored
seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
// the initializer loads rust libraries and helps with configuration
initializer = Initializer(App.instance)
// demonstrate deriving viewing keys for five accounts but only take the first one
viewingKey = initializer.deriveViewingKeys(seed).first()
// the derivation tool can be used for generating keys and addresses
viewingKey = DerivationTool.deriveViewingKeys(seed).first()
}
fun displayAddress() {
private fun displayAddress() {
// alternatively, `deriveAddress` can take the seed as a parameter instead
val address = initializer.deriveAddress(viewingKey)
binding.textInfo.text = address
// although, a full fledged app would just get the address from the synchronizer
val zaddress = DerivationTool.deriveShieldedAddress(viewingKey)
val taddress = DerivationTool.deriveTransparentAddress(seed)
binding.textInfo.text = "z-addr:\n$zaddress\n\n\nt-addr:\n$taddress"
}
// TODO: show an example with the synchronizer
@ -66,7 +63,10 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
//
override fun onActionButtonClicked() {
copyToClipboard(initializer.deriveAddress(viewingKey))
copyToClipboard(
DerivationTool.deriveTransparentAddress(seed),
"Shielded address copied to clipboard!"
)
}
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetAddressBinding =

View File

@ -1,12 +1,16 @@
package cash.z.ecc.android.sdk.demoapp.demos.getblock
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import cash.z.ecc.android.sdk.demoapp.App
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockBinding
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.demoapp.util.toHtml
import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime
import cash.z.ecc.android.sdk.demoapp.util.withCommas
import cash.z.ecc.android.sdk.ext.toHex
/**
* Retrieves a compact block from the lightwalletd service and displays basic information about it.
@ -14,39 +18,60 @@ import cash.z.ecc.android.sdk.service.LightWalletService
* the response.
*/
class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
private val host = App.instance.defaultConfig.host
private val port = App.instance.defaultConfig.port
private lateinit var lightwalletService: LightWalletService
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockBinding =
FragmentGetBlockBinding.inflate(layoutInflater)
fun resetInBackground() {
lightwalletService = LightWalletGrpcService(App.instance, host, port)
}
fun onResetComplete() {
binding.buttonApply.setOnClickListener(::onApply)
onApply(binding.textBlockHeight)
}
private fun onApply(_unused: View) {
setBlockHeight(binding.textBlockHeight.text.toString().toInt())
}
private fun setBlockHeight(blockHeight: Int) {
val blocks =
lightwalletService.getBlockRange(blockHeight..blockHeight)
val block = blocks.firstOrNull()
binding.textInfo.text = """
block height: ${block?.height}
block vtxCount: ${block?.vtxCount}
block time: ${block?.time}
lightwalletService?.getBlockRange(blockHeight..blockHeight)
val block = blocks?.firstOrNull()
binding.textInfo.visibility = View.VISIBLE
binding.textInfo.text = Html.fromHtml(
"""
<b>block height:</b> ${block?.height.withCommas()}
<br/><b>block time:</b> ${block?.time.toRelativeTime()}
<br/><b>number of shielded TXs:</b> ${block?.vtxCount}
<br/><b>hash:</b> ${block?.hash?.toByteArray()?.toHex()}
<br/><b>prevHash:</b> ${block?.prevHash?.toByteArray()?.toHex()}
${block?.vtxList.toHtml()}
""".trimIndent()
)
}
fun onClear() {
lightwalletService.shutdown()
private fun onApply(_unused: View? = null) {
try {
setBlockHeight(binding.textBlockHeight.text.toString().toInt())
} catch (t: Throwable) {
toast("Error: $t")
}
mainActivity()?.hideKeyboard()
}
private fun loadNext(offset: Int) {
val nextBlockHeight = (binding.textBlockHeight.text.toString().toIntOrNull() ?: -1) + offset
binding.textBlockHeight.setText(nextBlockHeight.toString())
onApply()
}
//
// Android Lifecycle overrides
//
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonApply.setOnClickListener(::onApply)
binding.buttonPrevious.setOnClickListener {
loadNext(-1)
}
binding.buttonNext.setOnClickListener {
loadNext(1)
}
}
//
// Base Fragment overrides
//
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockBinding =
FragmentGetBlockBinding.inflate(layoutInflater)
}

View File

@ -1,65 +1,113 @@
package cash.z.ecc.android.sdk.demoapp.demos.getblockrange
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import cash.z.ecc.android.sdk.demoapp.App
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockRangeBinding
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime
import cash.z.ecc.android.sdk.demoapp.util.withCommas
/**
* Retrieves a range of compact block from the lightwalletd service and displays basic information
* about them. This demonstrates the basic ability to connect to the server, request a range of
* about them. This demonstrates the basic ability to connect to the server, request a range of
* compact block and parse the response. This could be augmented to display metadata about certain
* block ranges for instance, to find the block with the most shielded transactions in a range.
*/
class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
private val host = App.instance.defaultConfig.host
private val port = App.instance.defaultConfig.port
private fun setBlockRange(blockRange: IntRange) {
val start = System.currentTimeMillis()
val blocks =
lightwalletService?.getBlockRange(blockRange)
val fetchDelta = System.currentTimeMillis() - start
private lateinit var lightwalletService: LightWalletService
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockRangeBinding =
FragmentGetBlockRangeBinding.inflate(layoutInflater)
// Note: This is a demo so we won't worry about iterating efficiently over these blocks
fun resetInBackground() {
lightwalletService = LightWalletGrpcService(App.instance, host, port)
}
binding.textInfo.text = Html.fromHtml(blocks?.run {
val count = size
val emptyCount = count { it.vtxCount == 0 }
val maxTxs = maxByOrNull { it.vtxCount }
val maxIns = maxByOrNull { block ->
block.vtxList.maxOfOrNull { it.spendsCount } ?: -1
}
val maxInTx = maxIns?.vtxList?.maxByOrNull { it.spendsCount }
val maxOuts = maxByOrNull { block ->
block.vtxList.maxOfOrNull { it.outputsCount } ?: -1
}
val maxOutTx = maxOuts?.vtxList?.maxByOrNull { it.outputsCount }
val txCount = sumBy { it.vtxCount }
val outCount = sumBy { block -> block.vtxList.sumBy { it.outputsCount } }
val inCount = sumBy { block -> block.vtxList.sumBy { it.spendsCount } }
fun onResetComplete() {
binding.buttonApply.setOnClickListener(::onApply)
onApply(binding.textInfo)
}
fun onClear() {
lightwalletService.shutdown()
val processTime = System.currentTimeMillis() - start - fetchDelta
"""
<b>total blocks:</b> ${count.withCommas()}
<br/><b>fetch time:</b> ${if(fetchDelta > 1000) "%.2f sec".format(fetchDelta/1000.0) else "%d ms".format(fetchDelta)}
<br/><b>process time:</b> ${if(processTime > 1000) "%.2f sec".format(processTime/1000.0) else "%d ms".format(processTime)}
<br/><b>block time range:</b> ${first().time.toRelativeTime()}<br/>&nbsp;&nbsp to ${last().time.toRelativeTime()}
<br/><b>total empty blocks:</b> ${emptyCount.withCommas()}
<br/><b>total TXs:</b> ${txCount.withCommas()}
<br/><b>total outputs:</b> ${outCount.withCommas()}
<br/><b>total inputs:</b> ${inCount.withCommas()}
<br/><b>avg TXs/block:</b> ${"%.1f".format(txCount/count.toDouble())}
<br/><b>avg TXs (excluding empty blocks):</b> ${"%.1f".format(txCount.toDouble()/(count - emptyCount))}
<br/><b>avg OUTs [per block / per TX]:</b> ${"%.1f / %.1f".format(outCount.toDouble()/(count - emptyCount), outCount.toDouble()/txCount)}
<br/><b>avg INs [per block / per TX]:</b> ${"%.1f / %.1f".format(inCount.toDouble()/(count - emptyCount), inCount.toDouble()/txCount)}
<br/><b>most shielded TXs:</b> ${if(maxTxs==null) "none" else "${maxTxs.vtxCount} in block ${maxTxs.height.withCommas()}"}
<br/><b>most shielded INs:</b> ${if(maxInTx==null) "none" else "${maxInTx.spendsCount} in block ${maxIns?.height.withCommas()} at tx index ${maxInTx.index}"}
<br/><b>most shielded OUTs:</b> ${if(maxOutTx==null) "none" else "${maxOutTx?.outputsCount} in block ${maxOuts?.height.withCommas()} at tx index ${maxOutTx?.index}"}
""".trimIndent()
} ?: "No blocks found in that range.")
}
private fun onApply(_unused: View) {
val start = binding.textStartHeight.text.toString().toInt()
val end = binding.textEndHeight.text.toString().toInt()
if (start <= end) {
setBlockRange(start..end)
try {
with(binding.buttonApply) {
isEnabled = false
setText(R.string.loading)
binding.textInfo.setText(R.string.loading)
post {
setBlockRange(start..end)
isEnabled = true
setText(R.string.apply)
}
}
} catch (t: Throwable) {
setError(t.toString())
}
} else {
setError("Invalid range")
}
}
// TODO: iterate on this demo to show all the blocks in a recyclerview showing block heights and vtx count
private fun setBlockRange(blockRange: IntRange) {
val blocks =
lightwalletService.getBlockRange(blockRange)
val block = blocks.firstOrNull()
binding.textInfo.text = """
block height: ${block?.height}
block vtxCount: ${block?.vtxCount}
block time: ${block?.time}
""".trimIndent()
mainActivity()?.hideKeyboard()
}
private fun setError(message: String) {
binding.textInfo.text = "Error: $message"
}
//
// Android Lifecycle overrides
//
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonApply.setOnClickListener(::onApply)
}
//
// Base Fragment overrides
//
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockRangeBinding =
FragmentGetBlockRangeBinding.inflate(layoutInflater)
}

View File

@ -1,41 +1,38 @@
package cash.z.ecc.android.sdk.demoapp.demos.getlatestheight
import android.view.LayoutInflater
import android.view.View
import cash.z.ecc.android.sdk.demoapp.App
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetLatestHeightBinding
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
/**
* Retrieves the latest block height from the lightwalletd server. This is the simplest test for
* connectivity with the server. Modify the `host` and the `port` to check the SDK's ability to
* communicate with a given lightwalletd instance.
* connectivity with the server. Modify the `host` and the `port` inside of
* `App.instance.defaultConfig` to check the SDK's ability to communicate with a given lightwalletd
* instance.
*/
class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>() {
private val host = App.instance.defaultConfig.host
private val port = App.instance.defaultConfig.port
private lateinit var lightwalletService: LightWalletService
private fun displayLatestHeight() {
// note: this is a blocking call, a real app wouldn't do this on the main thread
// instead, a production app would leverage the synchronizer like in the other demos
binding.textInfo.text = lightwalletService?.getLatestBlockHeight().toString()
}
//
// Android Lifecycle overrides
//
override fun onResume() {
super.onResume()
displayLatestHeight()
}
//
// Base Fragment overrides
//
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetLatestHeightBinding =
FragmentGetLatestHeightBinding.inflate(layoutInflater)
fun resetInBackground() {
lightwalletService = LightWalletGrpcService(App.instance, host, port)
}
fun onResetComplete() {
binding.textInfo.text = lightwalletService.getLatestBlockHeight().toString()
}
fun onClear() {
lightwalletService.shutdown()
}
override fun onActionButtonClicked() {
toast("Refreshed!")
onResetComplete()
}
}

View File

@ -4,10 +4,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.demoapp.App
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding
import cash.z.ecc.android.sdk.tool.DerivationTool
/**
* Displays the viewing key and spending key associated with the seed used during the demo. The
@ -16,7 +15,6 @@ import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding
*/
class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
private lateinit var initializer: Initializer
private lateinit var seedPhrase: String
private lateinit var seed: ByteArray
@ -24,10 +22,7 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
* Initialize the required values that would normally live outside the demo but are repeated
* here for completeness so that each demo file can serve as a standalone example.
*/
fun setup() {
// the initializer loads rust libraries and helps with configuration
initializer = Initializer(App.instance)
private fun setup() {
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
seedPhrase = sharedViewModel.seedPhrase.value
@ -36,11 +31,13 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
}
fun displayKeys() {
private fun displayKeys() {
// derive the keys from the seed:
// demonstrate deriving spending keys for five accounts but only take the first one
val spendingKey = initializer.deriveSpendingKeys(seed, 5).first()
val viewingKey = initializer.deriveViewingKey(spendingKey)
val spendingKey = DerivationTool.deriveSpendingKeys(seed, 5).first()
// derive the key that allows you to view but not spend transactions
val viewingKey = DerivationTool.deriveViewingKey(spendingKey)
// display the keys in the UI
binding.textInfo.setText("Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey")
@ -66,6 +63,13 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
// Base Fragment overrides
//
override fun onActionButtonClicked() {
copyToClipboard(
DerivationTool.deriveViewingKeys(seed, 1).first(),
"ViewingKey copied to clipboard!"
)
}
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetPrivateKeyBinding =
FragmentGetPrivateKeyBinding.inflate(layoutInflater)

View File

@ -41,12 +41,6 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
override fun onResume() {
super.onResume()
twig(
"CLEARING DATA: Visiting the home screen clears the default databases, for sanity" +
" sake, because each demo is intended to be self-contained."
)
// App.instance.getDatabasePath("unusued.db").parentFile?.listFiles()?.forEach { it.delete() }
mainActivity()?.setClipboardListener(::updatePasteButton)
lifecycleScope.launch {
@ -101,6 +95,7 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
private fun setEditShown(isShown: Boolean) {
with(binding) {
textSeedPhrase.visibility = if (isShown) View.GONE else View.VISIBLE
textInstructions.visibility = if (isShown) View.GONE else View.VISIBLE
groupEdit.visibility = if (isShown) View.VISIBLE else View.GONE
}
}

View File

@ -1,22 +1,22 @@
package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.VkInitializer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.demoapp.App
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.ext.twig
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import cash.z.ecc.android.sdk.tool.DerivationTool
/**
* List all transactions related to the given seed, since the given birthday. This begins by
@ -26,60 +26,46 @@ import kotlinx.coroutines.launch
* database in a paged format that works natively with RecyclerViews.
*/
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
private val config = App.instance.defaultConfig
private val initializer = Initializer(App.instance, host = config.host, port = config.port)
private val birthday = config.loadBirthday()
private lateinit var initializer: SdkSynchronizer.SdkInitializer
private lateinit var synchronizer: Synchronizer
private lateinit var adapter: TransactionAdapter<ConfirmedTransaction>
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)
fun resetInBackground() {
initializer.new(config.seed, birthday)
/**
* Initialize the required values that would normally live outside the demo but are repeated
* here for completeness so that each demo file can serve as a standalone example.
*/
private fun setup() {
App.instance.defaultConfig.let { config ->
initializer = VkInitializer(App.instance) { import(config.seed, config.birthdayHeight) }
address = DerivationTool.deriveShieldedAddress(config.seed)
}
synchronizer = Synchronizer(initializer)
}
fun onResetComplete() {
initTransactionUI()
startSynchronizer()
monitorStatus()
}
fun onClear() {
synchronizer.stop()
initializer.clear()
}
private fun initTransactionUI() {
binding.recyclerTransactions.layoutManager =
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = TransactionAdapter()
lifecycleScope.launch {
address = synchronizer.getAddress()
synchronizer.receivedTransactions.onEach {
onTransactionsUpdated(it)
}.launchIn(this)
}
binding.recyclerTransactions.adapter = adapter
}
private fun startSynchronizer() {
lifecycleScope.apply {
synchronizer.start(this)
}
}
private fun monitorStatus() {
private fun monitorChanges() {
// the lifecycleScope is used to stop everything when the fragment dies
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
}
//
// Change listeners
//
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
if (info.isScanning) binding.textInfo.text = "Scanning blocks...${info.scanProgress}%"
}
@ -108,8 +94,8 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
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"
"No transactions found. Try to either change the seed words " +
"or send funds to this address (tap the FAB to copy it):\n\n $address"
} else {
visibility = View.INVISIBLE
text = ""
@ -118,7 +104,38 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
}
}
//
// Android Lifecycle overrides
//
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setup()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initTransactionUI()
}
override fun onResume() {
super.onResume()
// the lifecycleScope is used to dispose of the synchronizer when the fragment dies
synchronizer.start(lifecycleScope)
monitorChanges()
}
//
// Base Fragment overrides
//
override fun onActionButtonClicked() {
if (::address.isInitialized) copyToClipboard(address)
}
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListTransactionsBinding =
FragmentListTransactionsBinding.inflate(layoutInflater)
}

View File

@ -1,10 +1,16 @@
package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
import android.content.res.ColorStateList
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.demoapp.App
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import java.text.SimpleDateFormat
import java.util.*
@ -16,14 +22,20 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
private val infoText = itemView.findViewById<TextView>(R.id.text_transaction_info)
private val timeText = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
private val icon = itemView.findViewById<ImageView>(R.id.image_transaction_type)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
fun bindTo(transaction: T?) {
val isInbound = transaction?.toAddress.isNullOrEmpty()
amountText.text = transaction?.value.convertZatoshiToZecString()
timeText.text =
if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending"
else formatter.format(transaction.blockTimeInSeconds * 1000L)
infoText.text = getMemoString(transaction)
icon.rotation = if (isInbound) 0f else 180f
icon.rotation = if (isInbound) 0f else 180f
icon.setColorFilter(ContextCompat.getColor(App.instance, if (isInbound) R.color.tx_inbound else R.color.tx_outbound))
}
private fun getMemoString(transaction: T?): String {

View File

@ -24,11 +24,11 @@ import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.rpc.LocalRpcTypes
import cash.z.ecc.android.sdk.tool.DerivationTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* List all transactions related to the given seed, since the given birthday. This begins by
* downloading any missing blocks and then validating and scanning their contents. Once scan is
@ -66,8 +66,6 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
initTransactionUi()
}
private lateinit var lightwalletService: LightWalletService
fun downloadTransactions() {
binding.textStatus.text = "loading..."
@ -76,14 +74,13 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
val addressToUse = binding.inputAddress.text.toString()
val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashSdk.SAPLING_ACTIVATION_HEIGHT
val endToUse = binding.inputRangeEnd.text.toString().toIntOrNull() ?: latestBlockHeight
lightwalletService = LightWalletGrpcService(App.instance, config.host, config.port)
var allStart = now
twig("loading transactions in range $startToUse..$endToUse")
val txids = lightwalletService.getTAddressTransactions(addressToUse, startToUse..endToUse)
val txids = lightwalletService?.getTAddressTransactions(addressToUse, startToUse..endToUse)
var delta = now - allStart
updateStatus("found ${txids.size} transactions in ${delta}ms.", false)
updateStatus("found ${txids?.size} transactions in ${delta}ms.", false)
txids.map {
txids?.map {
it.data.apply {
try {
initializer.rustBackend.decryptAndStoreTransaction(toByteArray())
@ -91,7 +88,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
twig("failed to decrypt and store transaction due to: $t")
}
}
}.let { txData ->
}?.let { txData ->
val parseStart = now
val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build()
val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList)
@ -136,7 +133,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
super.onResume()
resetInBackground()
val seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
binding.inputAddress.setText(initializer.rustBackend.deriveTAddress(seed))
binding.inputAddress.setText(DerivationTool.deriveTransparentAddress(seed))
}
@ -226,13 +223,6 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
private fun onTransactionsUpdated(transactions: PagedList<ConfirmedTransaction>) {
twig("got a new paged list of transactions of size ${transactions.size}")
adapter.submitList(transactions)
// show message when there are no transactions
// if (isSynced) {
if (transactions.isEmpty()) {
Toast.makeText(activity, "No transactions found. Try another address or send funds to this one.", Toast.LENGTH_LONG).show()
}
// }
}
override fun onActionButtonClicked() {

View File

@ -1,19 +1,21 @@
package cash.z.ecc.android.sdk.demoapp.demos.send
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.VkInitializer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.demoapp.App
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding
import cash.z.ecc.android.sdk.demoapp.util.SampleStorageBridge
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.ext.*
import cash.z.ecc.android.sdk.tool.DerivationTool
/**
* Demonstrates sending funds to an address. This is the most complex example that puts all of the
@ -24,16 +26,30 @@ import cash.z.ecc.android.sdk.ext.*
* Any time the state of that transaction changes, a new instance will be emitted.
*/
class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
private val config = App.instance.defaultConfig
private val initializer = Initializer(App.instance, host = config.host, port = config.port)
private val birthday = config.loadBirthday()
private lateinit var synchronizer: Synchronizer
private lateinit var keyManager: SampleStorageBridge
private lateinit var amountInput: TextView
private lateinit var addressInput: TextView
// in a normal app, this would be stored securely with the trusted execution environment (TEE)
// but since this is a demo, we'll derive it on the fly
private lateinit var spendingKey: String
/**
* Initialize the required values that would normally live outside the demo but are repeated
* here for completeness so that each demo file can serve as a standalone example.
*/
private fun setup() {
App.instance.defaultConfig.let { config ->
VkInitializer(App.instance) {
import(config.seed, config.birthdayHeight)
}.let { initializer ->
synchronizer = Synchronizer(initializer)
}
spendingKey = DerivationTool.deriveSpendingKeys(config.seed).first()
}
}
//
// Observable properties (done without livedata or flows for simplicity)
@ -57,52 +73,22 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
}
//
// BaseDemoFragment overrides
//
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentSendBinding =
FragmentSendBinding.inflate(layoutInflater)
fun resetInBackground() {
val spendingKeys = initializer.new(config.seed, birthday)
keyManager = SampleStorageBridge().securelyStorePrivateKey(spendingKeys[0])
synchronizer = Synchronizer(initializer)
}
// STARTING POINT
fun onResetComplete() {
initSendUi()
startSynchronizer()
monitorChanges()
}
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
App.instance.defaultConfig.let { config ->
amountInput = binding.inputAmount.apply {
setText(config.sendAmount.toZecString())
}
addressInput = binding.inputAddress.apply {
setText(config.toAddress)
}
}
binding.buttonSend.setOnClickListener(::onSend)
}
private fun startSynchronizer() {
lifecycleScope.apply {
synchronizer.start(this)
}
}
private fun monitorChanges() {
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
@ -110,6 +96,11 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
synchronizer.balances.collectWith(lifecycleScope, ::onBalance)
}
//
// Change listeners
//
private fun onStatus(status: Synchronizer.Status) {
binding.textStatus.text = "Status: $status"
isSyncing = status != Synchronizer.Status.SYNCED
@ -148,11 +139,12 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
val amount = amountInput.text.toString().toDouble().convertZecToZatoshi()
val toAddress = addressInput.text.toString().trim()
synchronizer.sendToAddress(
keyManager.key,
spendingKey,
amount,
toAddress,
"Demo App Funds"
"Funds from Demo App"
).collectWith(lifecycleScope, ::onPendingTxUpdated)
mainActivity()?.hideKeyboard()
}
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
@ -197,4 +189,33 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
binding.textInfo.text = "Active Transaction:"
}
//
// Android Lifecycle overrides
//
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setup()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initSendUi()
}
override fun onResume() {
super.onResume()
// the lifecycleScope is used to dispose of the synchronizer when the fragment dies
synchronizer.start(lifecycleScope)
monitorChanges()
}
//
// BaseDemoFragment overrides
//
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentSendBinding =
FragmentSendBinding.inflate(layoutInflater)
}

View File

@ -1,11 +1,46 @@
package cash.z.ecc.android.sdk.demoapp.util
import android.text.format.DateUtils
import androidx.fragment.app.Fragment
import cash.z.ecc.android.sdk.demoapp.App
import cash.z.ecc.android.sdk.demoapp.MainActivity
import cash.z.wallet.sdk.rpc.CompactFormats
/**
* Lazy extensions to make demo life easier.
*/
fun Fragment.mainActivity() = context as? MainActivity
fun Fragment.mainActivity() = context as? MainActivity
/**
* Add locale-specific commas to a number, if it exists.
*/
fun Number?.withCommas() = this?.let { "%,d".format(it) } ?: "Unknown"
/**
* Convert date time in seconds to relative time like (4 days ago).
*/
fun Int?.toRelativeTime() =
this?.let { timeInSeconds ->
DateUtils.getRelativeDateTimeString(
App.instance,
timeInSeconds * 1000L,
DateUtils.SECOND_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_ABBREV_MONTH
).toString()
} ?: "Unknown"
fun List<CompactFormats.CompactTx>?.toHtml() =
this.takeUnless { it.isNullOrEmpty() }?.let { txs ->
buildString {
append("<br/><b>transactions (shielded INs / OUTs):</b>")
txs.forEach { append("<br/><b>&nbsp;&nbsp;tx${it.index}:</b> ${it.spendsCount} / ${it.outputsCount}") }
}
} ?: ""

View File

@ -10,11 +10,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="20sp"
android:textSize="18sp"
android:text="loading address..."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.2"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,20 +9,26 @@
android:id="@+id/text_layout_block_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:endIconMode="clear_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_apply"
app:layout_constraintEnd_toEndOf="parent"
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.1">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_block_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="8"
android:background="@android:color/transparent"
android:ems="10"
android:hint="block height"
android:imeActionLabel="load"
android:imeOptions="actionDone"
android:inputType="number"
android:maxLines="1"
android:text="500000"
android:textSize="20sp" />
</com.google.android.material.textfield.TextInputLayout>
@ -31,21 +37,48 @@
android:id="@+id/button_apply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="apply"
app:layout_constraintBottom_toBottomOf="@id/text_layout_block_height"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_layout_block_height"
app:layout_constraintTop_toTopOf="@id/text_layout_block_height" />
<TextView
android:id="@+id/text_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="loading block..."
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
android:text="@string/load"
app:layout_constraintEnd_toStartOf="@id/button_previous"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_layout_block_height" />
<Button
android:id="@+id/button_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/previous"
app:layout_constraintEnd_toStartOf="@id/button_next"
app:layout_constraintStart_toEndOf="@id/button_apply"
app:layout_constraintTop_toTopOf="@id/button_apply" />
<Button
android:id="@+id/button_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_previous"
app:layout_constraintTop_toTopOf="@id/button_apply" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_apply">
<TextView
android:id="@+id/text_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="loading block..."
android:textSize="20sp"
android:visibility="gone" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -14,12 +14,13 @@
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.1">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_start_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="start height"
android:inputType="number"
android:maxLength="7"
@ -38,12 +39,13 @@
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/text_layout_start_height"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.2">
app:layout_constraintVertical_bias="0.1">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_end_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="end height"
android:inputType="number"
android:maxLength="7"
@ -62,15 +64,19 @@
app:layout_constraintStart_toEndOf="@id/text_layout_end_height"
app:layout_constraintTop_toTopOf="@id/text_layout_end_height" />
<TextView
android:id="@+id/text_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="loading blocks..."
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_layout_end_height" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_layout_start_height">
<TextView
android:id="@+id/text_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="loading blocks..."
android:textSize="19sp"/>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -10,7 +10,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="20sp"
android:textSize="18sp"
android:text="loading keys soon..."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -12,6 +12,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_baseline_edit_24"
android:drawableTint="@color/colorPrimary"
android:drawablePadding="12dp"
android:padding="24dp"
android:text="Seed phrase set to: apple...fish"
@ -77,6 +78,20 @@
app:layout_constraintEnd_toStartOf="@id/button_accept"
app:layout_constraintTop_toBottomOf="@id/text_layout_seed_phrase" />
<TextView
android:id="@+id/text_instructions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Or modify the seed phrase\nused for all demos, below:"
android:gravity="center"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_home"
app:layout_constraintBottom_toTopOf="@id/text_seed_phrase"
/>
<TextView
android:id="@+id/text_home"
android:layout_width="wrap_content"

View File

@ -3,12 +3,14 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_send"
app:layout_constraintHorizontal_chainStyle="packed"
@ -20,27 +22,30 @@
android:id="@+id/input_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:ems="8"
android:hint="zec amount"
android:inputType="number"
android:textSize="20sp" />
android:textSize="18sp" />
</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_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/text_layout_amount"
app:layout_constraintEnd_toEndOf="parent"
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_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:maxLines="2"
android:ems="8"
android:hint="to address"
android:textSize="20sp" />
android:textSize="18sp" />
</com.google.android.material.textfield.TextInputLayout>
<Button
@ -50,9 +55,7 @@
android:enabled="false"
android:text="Send"
app:layout_constraintBottom_toBottomOf="@id/text_layout_amount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_layout_amount"
app:layout_constraintTop_toTopOf="@id/text_layout_amount" />
app:layout_constraintStart_toEndOf="@id/text_layout_amount"/>
<TextView
android:id="@+id/text_status"
@ -75,17 +78,17 @@
<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"
>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/text_balance"
app:layout_constraintTop_toBottomOf="@id/text_balance">
<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"/>
android:paddingBottom="48dp"
android:paddingEnd="8dp" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,7 +5,7 @@
android:id="@+id/container_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#F0F0F0"
android:background="#60F0F0F0"
android:elevation="1dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
@ -42,7 +42,7 @@
android:layout_height="wrap_content"
android:textSize="12sp"
android:paddingEnd="16dp"
android:maxLines="1"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintEnd_toStartOf="@id/text_transaction_amount"
app:layout_constraintStart_toStartOf="@id/text_transaction_timestamp"

View File

@ -3,4 +3,7 @@
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
<color name="tx_outbound">#F06292</color>
<color name="tx_inbound">#81C784</color>
</resources>

View File

@ -21,4 +21,7 @@
<string name="home_second">Home Second</string>
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="load">Load</string>
<string name="apply">Apply</string>
<string name="loading">⌛ Loading</string>
</resources>

View File

@ -1,13 +1,14 @@
package cash.z.ecc.android.sdk.demoapp
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.demoapp.util.SimpleMnemonics
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
data class DemoConfig(
val alias: String = "SdkDemo",
val host: String = "lightwalletd.electriccoin.co",
val port: Int = 9067,
val birthdayHeight: Int = 835_000,
val sendAmount: Double = 0.0018,
val birthdayHeight: Int = 968000,
val sendAmount: Double = 0.000018,
// 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",
@ -16,6 +17,6 @@ data class DemoConfig(
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)
fun newWalletBirthday() = WalletBirthdayTool.loadNearest(App.instance)
fun loadBirthday(height: Int = birthdayHeight) = WalletBirthdayTool.loadNearest(App.instance, height)
}

View File

@ -3,18 +3,18 @@
buildscript {
ext.kotlin_version = '1.3.72'
ext.kotlin_version = '1.4.0'
ext.sdk_version = '1.0.0-alpha03'
repositories {
google ()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.android.tools.build:gradle:4.0.1'
classpath"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.2.2'
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0'
}
}