Initial commit

This commit is contained in:
fekt
2022-06-30 01:53:12 -04:00
commit 0131016214
703 changed files with 33394 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
package cash.z.wallet.sdk.sample.demoapp
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.isFailure
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.toHex
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
/**
* Sample code to demonstrate key functionality without UI, inspired by:
* https://github.com/EdgeApp/eosjs-node-cli/blob/paul/cleanup/app.js
*/
class SampleCodeTest {
// ///////////////////////////////////////////////////
// Seed derivation
@Ignore("This test is not implemented")
@Test
fun createBip39Seed_fromSeedPhrase() {
// TODO: log(seedPhrase.asRawEntropy().asBip39seed())
}
@Ignore("This test is not implemented")
@Test
fun createRawEntropy() {
// TODO: call: Mnemonic::from_phrase(seed_phrase, Language::English).unwrap().entropy()
// log(seedPhrase.asRawEntropy())
}
@Ignore("This test is not implemented")
@Test
fun createBip39Seed_fromRawEntropy() {
// get the 64 byte bip39 entropy
// TODO: call: bip39::Seed::new(&Mnemonic::from_entropy(&seed_bytes, Language::English).unwrap(), "")
// log(rawEntropy.asBip39Seed())
}
@Ignore("This test is not implemented")
@Test
fun deriveSeedPhraseFrom() {
// TODO: let mnemonic = Mnemonic::from_entropy(entropy, Language::English).unwrap();
// log(entropy.asSeedPhrase())
}
// ///////////////////////////////////////////////////
// Derive Extended Spending Key
@Test fun deriveSpendingKey() {
val spendingKeys = runBlocking {
DerivationTool.deriveSpendingKeys(
seed,
ZcashNetwork.Mainnet
)
}
assertEquals(1, spendingKeys.size)
log("Spending Key: ${spendingKeys?.get(0)}")
}
// ///////////////////////////////////////////////////
// Get Address
@Test fun getAddress() = runBlocking {
val address = synchronizer.getAddress()
assertFalse(address.isNullOrBlank())
log("Address: $address")
}
// ///////////////////////////////////////////////////
// Derive address from Extended Full Viewing Key
@Test fun getAddressFromViewingKey() {
}
// ///////////////////////////////////////////////////
// Query latest block height
@Test fun getLatestBlockHeightTest() {
val lightwalletService = LightWalletGrpcService(context, lightwalletdHost)
log("Latest Block: ${lightwalletService.getLatestBlockHeight()}")
}
// ///////////////////////////////////////////////////
// Download compact block range
@Test fun getBlockRange() {
val blockRange = 500_000..500_009
val lightwalletService = LightWalletGrpcService(context, lightwalletdHost)
val blocks = lightwalletService.getBlockRange(blockRange)
assertEquals(blockRange.count(), blocks.size)
blocks.forEachIndexed { i, block ->
log("Block #$i: height:${block.height} hash:${block.hash.toByteArray().toHex()}")
}
}
// ///////////////////////////////////////////////////
// Query account outgoing transactions
@Test fun queryOutgoingTransactions() {
}
// ///////////////////////////////////////////////////
// Query account incoming transactions
@Test fun queryIncomingTransactions() {
}
// // ///////////////////////////////////////////////////
// // Create a signed transaction (with memo)
// @Test fun createTransaction() = runBlocking {
// val rustBackend = RustBackend.init(context)
// val repository = PagedTransactionRepository(context)
// val encoder = WalletTransactionEncoder(rustBackend, repository)
// val spendingKey = DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.Mainnet)[0]
//
// val amount = 0.123.convertZecToZatoshi()
// val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw"
// val memo = "Test Transaction".toByteArray()
//
// val encodedTx = encoder.createTransaction(spendingKey, amount, address, memo)
// assertTrue(encodedTx.raw.isNotEmpty())
// log("Transaction ID: ${encodedTx.txId.toHex()}")
// }
// ///////////////////////////////////////////////////
// Create a signed transaction (with memo) and broadcast
@Test fun submitTransaction() = runBlocking {
val amount = 0.123.convertZecToZatoshi()
val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw"
val memo = "Test Transaction"
val spendingKey = DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.Mainnet)[0]
val transactionFlow = synchronizer.sendToAddress(spendingKey, amount, address, memo)
transactionFlow.collect {
log("pending transaction updated $it")
assertTrue("Failed to send funds. See log for details.", !it?.isFailure())
}
}
// /////////////////////////////////////////////////////
// Utility Functions
// ////////////////////////////////////////////////////
companion object {
private val seed = "Insert seed for testing".toByteArray()
private val lightwalletdHost: String = ZcashNetwork.Mainnet.defaultHost
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val synchronizer: Synchronizer = run {
val initializer = runBlocking { Initializer.new(context) {} }
Synchronizer.newBlocking(initializer)
}
@BeforeClass
@JvmStatic
fun init() {
Twig.plant(TroubleshootingTwig())
}
fun log(message: String?) = twig(message ?: "null")
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cash.z.ecc.android.sdk.demoapp">
<application
android:name=".App"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,18 @@
package cash.z.ecc.android.sdk.demoapp
import android.app.Application
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictModeHelper.enableStrictMode()
}
Twig.plant(TroubleshootingTwig())
}
}

View File

@@ -0,0 +1,97 @@
package cash.z.ecc.android.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.fragment.app.activityViewModels
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
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 onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = inflateBinding(layoutInflater)
return binding.root
}
override fun onResume() {
super.onResume()
registerActionButtonListener()
}
override fun onPause() {
super.onPause()
unregisterActionButtonListener()
(activity as? MainActivity)?.hideKeyboard()
}
private fun registerActionButtonListener() {
(activity as? MainActivity)?.fabListener = this
}
private fun unregisterActionButtonListener() {
(activity as? MainActivity)?.apply {
if (fabListener === this@BaseDemoFragment) fabListener = null
}
}
/**
* 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(requireView(), "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, description: String = "Copied to clipboard!") {
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)?.let { cm ->
cm.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text))
}
toast(description)
}
/**
* 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.
*/
abstract fun inflateBinding(layoutInflater: LayoutInflater): T
}

View File

@@ -0,0 +1,161 @@
package cash.z.ecc.android.sdk.demoapp
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.getSystemService
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
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.demoapp.util.fromResources
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.type.ZcashNetwork
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView
class MainActivity :
AppCompatActivity(),
ClipboardManager.OnPrimaryClipChangedListener,
DrawerLayout.DrawerListener {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var clipboard: ClipboardManager
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
clipboard.addPrimaryClipChangedListener(this)
setContentView(R.layout.activity_main)
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
val fab: FloatingActionButton = findViewById(R.id.fab)
fab.setOnClickListener { view ->
onFabClicked(view)
}
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
val navView: NavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.nav_home, R.id.nav_address, R.id.nav_balance, R.id.nav_block, R.id.nav_private_key,
R.id.nav_latest_height, R.id.nav_block_range,
R.id.nav_transactions, R.id.nav_utxos, R.id.nav_send
),
drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
drawerLayout.addDrawerListener(this)
initService()
}
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)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.action_settings) {
val navController = findNavController(R.id.nav_host_fragment)
navController.navigate(R.id.nav_home)
true
} else {
super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
//
// Private functions
//
private fun initService() {
if (lightwalletService != null) {
lightwalletService?.shutdown()
}
lightwalletService = LightWalletGrpcService(applicationContext, ZcashNetwork.fromResources(applicationContext))
}
private fun onFabClicked(view: View) {
fabListener?.onActionButtonClicked()
}
//
// Helpers
//
fun getClipboardText(): String? {
return with(clipboard) {
if (!hasPrimaryClip()) return null
return primaryClip!!.getItemAt(0)?.coerceToText(this@MainActivity)?.toString()
}
}
override fun onPrimaryClipChanged() {
clipboardListener?.invoke(getClipboardText())
}
fun setClipboardListener(block: (String?) -> Unit) {
clipboardListener = block
block(getClipboardText())
}
fun removeClipboardListener() {
clipboardListener = null
}
fun hideKeyboard() {
val windowToken = window.decorView.rootView.windowToken
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(windowToken, 0)
}
/* DrawerListener implementation */
override fun onDrawerStateChanged(newState: Int) {
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
}
override fun onDrawerClosed(drawerView: View) {
}
override fun onDrawerOpened(drawerView: View) {
hideKeyboard()
}
}

View File

@@ -0,0 +1,34 @@
package cash.z.ecc.android.sdk.demoapp
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.bip39.Mnemonics
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Shared mutable state for the demo
*/
class SharedViewModel : ViewModel() {
private val _seedPhrase = MutableStateFlow(DemoConstants.initialSeedWords)
// publicly, this is read-only
val seedPhrase: StateFlow<String> get() = _seedPhrase
fun updateSeedPhrase(newPhrase: String?): Boolean {
return if (isValidSeedPhrase(newPhrase)) {
_seedPhrase.value = newPhrase!!
true
} else {
false
}
}
private fun isValidSeedPhrase(phrase: String?): Boolean {
if (phrase.isNullOrEmpty()) return false
return try {
Mnemonics.MnemonicCode(phrase).validate()
true
} catch (t: Throwable) { false }
}
}

View File

@@ -0,0 +1,60 @@
package cash.z.ecc.android.sdk.demoapp
import android.annotation.SuppressLint
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.StrictMode
object StrictModeHelper {
fun enableStrictMode() {
configureStrictMode()
// Workaround for Android bug
// https://issuetracker.google.com/issues/36951662
// Not needed if target O_MR1 and running on O_MR1
// Don't really need to check target, because of Google Play enforcement on targetSdkVersion for app updates
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
Handler(Looper.getMainLooper()).postAtFrontOfQueue { configureStrictMode() }
}
}
@SuppressLint("NewApi")
private fun configureStrictMode() {
StrictMode.enableDefaults()
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder().apply {
detectAll()
penaltyLog()
}.build()
)
// Don't enable missing network tags, because those are noisy.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder().apply {
detectActivityLeaks()
detectCleartextNetwork()
detectContentUriWithoutPermission()
detectFileUriExposure()
detectLeakedClosableObjects()
detectLeakedRegistrationObjects()
detectLeakedSqlLiteObjects()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Disable because this is mostly flagging Android X and Play Services
// builder.detectNonSdkApiUsage();
}
}.build()
)
} else {
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder().apply {
detectAll()
penaltyLog()
}.build()
)
}
}
}

View File

@@ -0,0 +1,86 @@
package cash.z.ecc.android.sdk.demoapp.demos.getaddress
import android.os.Bundle
import android.view.LayoutInflater
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
/**
* Displays the address associated with the seed defined by the default config. To modify the seed
* that is used, update the `DemoConfig.seedWords` value.
*/
class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
private lateinit var viewingKey: UnifiedViewingKey
private lateinit var seed: ByteArray
/**
* 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() {
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
val seedPhrase = sharedViewModel.seedPhrase.value
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
// have the seed stored
seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
// the derivation tool can be used for generating keys and addresses
viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }
}
private fun displayAddress() {
// a full fledged app would just get the address from the synchronizer
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
val zaddress = DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))
val taddress = DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))
binding.textInfo.text = "z-addr:\n$zaddress\n\n\nt-addr:\n$taddress"
}
}
// TODO: show an example with the synchronizer
//
// Android Lifecycle overrides
//
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setup()
}
override fun onResume() {
super.onResume()
displayAddress()
}
//
// Base Fragment overrides
//
override fun onActionButtonClicked() {
viewLifecycleOwner.lifecycleScope.launch {
copyToClipboard(
DerivationTool.deriveShieldedAddress(
viewingKey.extfvk,
ZcashNetwork.fromResources(requireApplicationContext())
),
"Shielded address copied to clipboard!"
)
}
}
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetAddressBinding =
FragmentGetAddressBinding.inflate(layoutInflater)
}

View File

@@ -0,0 +1,101 @@
package cash.z.ecc.android.sdk.demoapp.demos.getbalance
import android.os.Bundle
import android.view.LayoutInflater
import androidx.lifecycle.lifecycleScope
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.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.runBlocking
/**
* Displays the available balance && total balance associated with the seed defined by the default config.
* comments.
*/
class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
private lateinit var synchronizer: Synchronizer
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBalanceBinding =
FragmentGetBalanceBinding.inflate(layoutInflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setup()
}
private fun setup() {
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
val seedPhrase = sharedViewModel.seedPhrase.value
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
// have the seed stored
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
// converting seed into viewingKey
val viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }
// using the ViewingKey to initialize
runBlocking {
Initializer.new(requireApplicationContext(), null) {
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
it.importWallet(viewingKey, network = ZcashNetwork.fromResources(requireApplicationContext()))
}
}.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer)
}
}
override fun onResume() {
super.onResume()
// the lifecycleScope is used to dispose of the synchronize when the fragment dies
synchronizer.start(lifecycleScope)
monitorChanges()
}
private fun monitorChanges() {
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
synchronizer.saplingBalances.filterNotNull().collectWith(lifecycleScope, ::onBalance)
}
private fun onBalance(balance: WalletBalance) {
binding.textBalance.text = """
Available balance: ${balance.available.convertZatoshiToZecString(12)}
Total balance: ${balance.total.convertZatoshiToZecString(12)}
""".trimIndent()
}
private fun onStatus(status: Synchronizer.Status) {
binding.textStatus.text = "Status: $status"
val balance = synchronizer.saplingBalances.value
if (null == balance) {
binding.textBalance.text = "Calculating balance..."
} else {
onBalance(balance)
}
}
private fun onProgress(i: Int) {
if (i < 100) {
binding.textStatus.text = "Downloading blocks...$i%"
}
}
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
}
}

View File

@@ -0,0 +1,76 @@
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.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
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.
* This demonstrates the basic ability to connect to the server, request a compact block and parse
* the response.
*/
class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
private fun setBlockHeight(blockHeight: Int) {
val blocks =
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(requireApplicationContext())}
<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()
)
}
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

@@ -0,0 +1,118 @@
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.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockRangeBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
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
* 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 fun setBlockRange(blockRange: IntRange) {
val start = System.currentTimeMillis()
val blocks =
lightwalletService?.getBlockRange(blockRange)
val fetchDelta = System.currentTimeMillis() - start
// Note: This is a demo so we won't worry about iterating efficiently over these blocks
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 } }
val processTime = System.currentTimeMillis() - start - fetchDelta
@Suppress("MaxLineLength")
"""
<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(requireApplicationContext())}<br/>&nbsp;&nbsp to ${last().time.toRelativeTime(requireApplicationContext())}
<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) {
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")
}
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)
override fun onActionButtonClicked() {
super.onActionButtonClicked()
}
}

View File

@@ -0,0 +1,36 @@
package cash.z.ecc.android.sdk.demoapp.demos.getlatestheight
import android.view.LayoutInflater
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetLatestHeightBinding
/**
* 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` inside of
* `App.instance.defaultConfig` to check the SDK's ability to communicate with a given lightwalletd
* instance.
*/
class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>() {
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)
}

View File

@@ -0,0 +1,92 @@
package cash.z.ecc.android.sdk.demoapp.demos.getprivatekey
import android.os.Bundle
import android.view.LayoutInflater
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.launch
/**
* Displays the viewing key and spending key associated with the seed used during the demo. The
* seedPhrase defaults to the value of`DemoConfig.seedWords` but can be set by the user on the
* HomeFragment.
*/
class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
private lateinit var seedPhrase: String
private lateinit var seed: ByteArray
/**
* 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() {
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
seedPhrase = sharedViewModel.seedPhrase.value
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
// have the seed stored
seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
}
private fun displayKeys() {
// derive the keys from the seed:
// demonstrate deriving spending keys for five accounts but only take the first one
lifecycleScope.launchWhenStarted {
val spendingKey = DerivationTool.deriveSpendingKeys(
seed,
ZcashNetwork.fromResources(requireApplicationContext()),
5
).first()
// derive the key that allows you to view but not spend transactions
val viewingKey = DerivationTool.deriveViewingKey(
spendingKey,
ZcashNetwork.fromResources(requireApplicationContext())
)
// display the keys in the UI
binding.textInfo.setText("Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey")
}
}
//
// Android Lifecycle overrides
//
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setup()
}
override fun onResume() {
super.onResume()
displayKeys()
}
//
// Base Fragment overrides
//
override fun onActionButtonClicked() {
lifecycleScope.launch {
copyToClipboard(
DerivationTool.deriveUnifiedViewingKeys(
seed,
ZcashNetwork.fromResources(requireApplicationContext())
).first().extpub,
"ViewingKey copied to clipboard!"
)
}
}
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetPrivateKeyBinding =
FragmentGetPrivateKeyBinding.inflate(layoutInflater)
}

View File

@@ -0,0 +1,117 @@
package cash.z.ecc.android.sdk.demoapp.demos.home
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentHomeBinding
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/**
* The landing page for the demo. Every time the app returns to this screen, it clears all demo
* data just for sanity. The goal is for each demo to be self-contained so that the behavior is
* repeatable and independent of pre-existing state.
*/
class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
private val homeViewModel: HomeViewModel by viewModels()
override fun inflateBinding(layoutInflater: LayoutInflater) =
FragmentHomeBinding.inflate(layoutInflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textSeedPhrase.setOnClickListener(::onEditSeedPhrase)
binding.buttonPaste.setOnClickListener(::onPasteSeedPhrase)
binding.buttonAccept.setOnClickListener(::onAcceptSeedPhrase)
binding.buttonCancel.setOnClickListener(::onCancelSeedPhrase)
}
override fun onResume() {
super.onResume()
mainActivity()?.setClipboardListener(::updatePasteButton)
lifecycleScope.launch {
sharedViewModel.seedPhrase.collect {
binding.textSeedPhrase.text = "Seed Phrase: ${it.toAbbreviatedPhrase()}"
}
}
}
override fun onPause() {
super.onPause()
mainActivity()?.removeClipboardListener()
}
private fun onEditSeedPhrase(unused: View) {
setEditShown(true)
binding.inputSeedPhrase.setText(sharedViewModel.seedPhrase.value)
binding.textLayoutSeedPhrase.helperText = ""
}
private fun onAcceptSeedPhrase(unused: View) {
if (applySeedPhrase()) {
setEditShown(false)
binding.inputSeedPhrase.setText("")
}
}
private fun onCancelSeedPhrase(unused: View) {
setEditShown(false)
}
private fun onPasteSeedPhrase(unused: View) {
mainActivity()?.getClipboardText().let { clipboardText ->
binding.inputSeedPhrase.setText(clipboardText)
applySeedPhrase()
}
}
private fun applySeedPhrase(): Boolean {
val newPhrase = binding.inputSeedPhrase.text.toString()
return if (!sharedViewModel.updateSeedPhrase(newPhrase)) {
binding.textLayoutSeedPhrase.helperText = "Invalid seed phrase"
binding.textLayoutSeedPhrase.setHelperTextColor(ColorStateList.valueOf(Color.RED))
false
} else {
binding.textLayoutSeedPhrase.helperText = "valid seed phrase"
binding.textLayoutSeedPhrase.setHelperTextColor(ColorStateList.valueOf(Color.GREEN))
true
}
}
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
}
}
private fun updatePasteButton(clipboardText: String? = mainActivity()?.getClipboardText()) {
clipboardText.let {
val isEditing = binding.groupEdit.visibility == View.VISIBLE
if (isEditing && (it != null && it.split(' ').size > 2)) {
binding.buttonPaste.visibility = View.VISIBLE
} else {
binding.buttonPaste.visibility = View.GONE
}
}
}
private fun String.toAbbreviatedPhrase(): String {
this.trim().apply {
val firstSpace = indexOf(' ')
val lastSpace = lastIndexOf(' ')
return if (firstSpace != -1 && lastSpace >= firstSpace) {
"${take(firstSpace)}...${takeLast(length - 1 - lastSpace)}"
} else this
}
}
}

View File

@@ -0,0 +1,5 @@
package cash.z.ecc.android.sdk.demoapp.demos.home
import androidx.lifecycle.ViewModel
class HomeViewModel : ViewModel()

View File

@@ -0,0 +1,154 @@
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.recyclerview.widget.LinearLayoutManager
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.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
/**
* 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
* complete, the transactions are available in the database and can be accessed by any SQL tool.
* By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the
* database in a paged format that works natively with RecyclerViews.
*/
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
private lateinit var initializer: Initializer
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
/**
* 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() {
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
var seedPhrase = sharedViewModel.seedPhrase.value
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
// have the seed stored
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
initializer = runBlocking {
Initializer.new(requireApplicationContext()) {
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
}
}
address = runBlocking {
DerivationTool.deriveShieldedAddress(
seed,
ZcashNetwork.fromResources(requireApplicationContext())
)
}
synchronizer = Synchronizer.newBlocking(initializer)
}
private fun initTransactionUI() {
binding.recyclerTransactions.layoutManager =
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = TransactionAdapter()
binding.recyclerTransactions.adapter = adapter
}
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}%"
}
private fun onProgress(i: Int) {
if (i < 100) binding.textInfo.text = "Downloading blocks...$i%"
}
private fun onStatus(status: Synchronizer.Status) {
this.status = status
binding.textStatus.text = "Status: $status"
if (isSynced) onSyncComplete()
}
private fun onSyncComplete() {
binding.textInfo.visibility = View.INVISIBLE
}
private fun onTransactionsUpdated(transactions: List<ConfirmedTransaction>) {
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 " +
"or send funds to this address (tap the FAB to copy it):\n\n $address"
} else {
visibility = View.INVISIBLE
text = ""
}
}
}
}
//
// 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

@@ -0,0 +1,39 @@
package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.demoapp.R
/**
* Simple adapter implementation that knows how to bind a recyclerview to ClearedTransactions.
*/
class TransactionAdapter<T : ConfirmedTransaction> :
ListAdapter<T, TransactionViewHolder<T>>(
object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(
oldItem: T,
newItem: T
) = oldItem.minedHeight == newItem.minedHeight
override fun areContentsTheSame(
oldItem: T,
newItem: T
) = oldItem == newItem
}
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = TransactionViewHolder<T>(
LayoutInflater.from(parent.context).inflate(R.layout.item_transaction, parent, false)
)
override fun onBindViewHolder(
holder: TransactionViewHolder<T>,
position: Int
) = holder.bindTo(getItem(position))
}

View File

@@ -0,0 +1,41 @@
package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import java.text.SimpleDateFormat
import java.util.Locale
/**
* Simple view holder for displaying confirmed transactions in the recyclerview.
*/
class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
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?.valueInZatoshi.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(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound))
}
private fun getMemoString(transaction: T?): String {
return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo"
}
}

View File

@@ -0,0 +1,249 @@
package cash.z.ecc.android.sdk.demoapp.demos.listutxos
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
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.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.DemoConstants
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListUtxosBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
/**
* ===============================================================================================
* NOTE: this is still a WIP because t-addrs are not officially supported by the SDK yet
* ===============================================================================================
*
*
* 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
* complete, the transactions are available in the database and can be accessed by any SQL tool.
* By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the
* database in a paged format that works natively with RecyclerViews.
*/
class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
private lateinit var seed: ByteArray
private lateinit var initializer: Initializer
private lateinit var synchronizer: Synchronizer
private lateinit var adapter: UtxoAdapter<ConfirmedTransaction>
private val address: String = "t1RwbKka1CnktvAJ1cSqdn7c6PXWG4tZqgd"
private var status: Synchronizer.Status? = null
private val isSynced get() = status == Synchronizer.Status.SYNCED
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListUtxosBinding =
FragmentListUtxosBinding.inflate(layoutInflater)
/**
* 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() {
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
// have the seed stored
seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
initializer = runBlocking {
Initializer.new(requireApplicationContext()) {
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
it.alias = "Demo_Utxos"
}
}
synchronizer = runBlocking { Synchronizer.new(initializer) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setup()
}
fun initUi() {
binding.inputAddress.setText(address)
binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString())
binding.inputRangeEnd.setText(DemoConstants.utxoEndHeight.toString())
binding.buttonLoad.setOnClickListener {
mainActivity()?.hideKeyboard()
downloadTransactions()
}
initTransactionUi()
}
fun downloadTransactions() {
binding.textStatus.text = "loading..."
binding.textStatus.post {
binding.textStatus.requestFocus()
val addressToUse = binding.inputAddress.text.toString()
val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight
val endToUse = binding.inputRangeEnd.text.toString().toIntOrNull() ?: DemoConstants.utxoEndHeight
var allStart = now
twig("loading transactions in range $startToUse..$endToUse")
val txids = lightwalletService?.getTAddressTransactions(addressToUse, startToUse..endToUse)
var delta = now - allStart
updateStatus("found ${txids?.size} transactions in ${delta}ms.", false)
txids?.map {
it.data.apply {
try {
runBlocking { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) }
} catch (t: Throwable) {
twig("failed to decrypt and store transaction due to: $t")
}
}
}?.let { txData ->
// Disabled during migration to newer SDK version; this appears to have been
// leveraging non-public APIs in the SDK so perhaps should be removed
// val parseStart = now
// val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build()
// val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList)
// delta = now - parseStart
// updateStatus("parsed txs in ${delta}ms.")
}
(synchronizer as SdkSynchronizer).refreshTransactions()
// val finalCount = (synchronizer as SdkSynchronizer).getTransactionCount()
// "found ${finalCount - initialCount} shielded outputs.
delta = now - allStart
updateStatus("Total time ${delta}ms.")
lifecycleScope.launch {
withContext(Dispatchers.IO) {
finalCount = (synchronizer as SdkSynchronizer).getTransactionCount()
withContext(Dispatchers.Main) {
delay(100)
updateStatus("Also found ${finalCount - initialCount} shielded txs")
}
}
}
}
}
private val now get() = System.currentTimeMillis()
private fun updateStatus(message: String, append: Boolean = true) {
if (append) {
binding.textStatus.text = "${binding.textStatus.text} $message"
} else {
binding.textStatus.text = message
}
twig(message)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUi()
}
override fun onResume() {
super.onResume()
resetInBackground()
val seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
binding.inputAddress.setText(DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext())))
}
}
var initialCount: Int = 0
var finalCount: Int = 0
fun resetInBackground() {
try {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
synchronizer.prepare()
initialCount = (synchronizer as SdkSynchronizer).getTransactionCount()
}
}
synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
// synchronizer.receivedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
} catch (t: Throwable) {
twig("failed to start the synchronizer!!! due to : $t")
}
}
fun onResetComplete() {
initTransactionUi()
startSynchronizer()
monitorStatus()
}
fun onClear() {
synchronizer.stop()
}
private fun initTransactionUi() {
binding.recyclerTransactions.layoutManager =
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = UtxoAdapter()
binding.recyclerTransactions.adapter = adapter
// lifecycleScope.launch {
// // address = synchronizer.getAddress()
// synchronizer.receivedTransactions.onEach {
// onTransactionsUpdated(it)
// }.launchIn(this)
// }
}
private fun startSynchronizer() {
lifecycleScope.apply {
synchronizer.start(this)
}
}
private fun monitorStatus() {
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
}
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
}
private fun onProgress(i: Int) {
if (i < 100) binding.textStatus.text = "Downloading blocks...$i%"
}
private fun onStatus(status: Synchronizer.Status) {
this.status = status
binding.textStatus.text = "Status: $status"
if (isSynced) onSyncComplete()
}
private fun onSyncComplete() {
binding.textStatus.visibility = View.INVISIBLE
}
private fun onTransactionsUpdated(transactions: List<ConfirmedTransaction>) {
twig("got a new paged list of transactions of size ${transactions.size}")
adapter.submitList(transactions)
}
override fun onActionButtonClicked() {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
twig("current count: ${(synchronizer as SdkSynchronizer).getTransactionCount()}")
twig("refreshing transactions")
(synchronizer as SdkSynchronizer).refreshTransactions()
twig("current count: ${(synchronizer as SdkSynchronizer).getTransactionCount()}")
}
}
}
}

View File

@@ -0,0 +1,39 @@
package cash.z.ecc.android.sdk.demoapp.demos.listutxos
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.demoapp.R
/**
* Simple adapter implementation that knows how to bind a recyclerview to ClearedTransactions.
*/
class UtxoAdapter<T : ConfirmedTransaction> :
ListAdapter<T, UtxoViewHolder<T>>(
object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(
oldItem: T,
newItem: T
) = oldItem.minedHeight == newItem.minedHeight
override fun areContentsTheSame(
oldItem: T,
newItem: T
) = oldItem == newItem
}
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = UtxoViewHolder<T>(
LayoutInflater.from(parent.context).inflate(R.layout.item_transaction, parent, false)
)
override fun onBindViewHolder(
holder: UtxoViewHolder<T>,
position: Int
) = holder.bindTo(getItem(position))
}

View File

@@ -0,0 +1,33 @@
package cash.z.ecc.android.sdk.demoapp.demos.listutxos
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import java.text.SimpleDateFormat
import java.util.Locale
/**
* Simple view holder for displaying confirmed transactions in the recyclerview.
*/
class UtxoViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
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 formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
fun bindTo(transaction: T?) {
amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
timeText.text =
if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending"
else formatter.format(transaction.blockTimeInSeconds * 1000L)
infoText.text = getMemoString(transaction)
}
private fun getMemoString(transaction: T?): String {
return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo"
}
}

View File

@@ -0,0 +1,239 @@
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.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCreated
import cash.z.ecc.android.sdk.db.entity.isCreating
import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
import cash.z.ecc.android.sdk.db.entity.isFailedSubmit
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.DemoConstants
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.toZecString
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
/**
* Demonstrates sending funds to an address. This is the most complex example that puts all of the
* pieces of the SDK together, including monitoring transactions for completion. It begins by
* downloading, validating and scanning any missing blocks. Once that is complete, the wallet is
* in a SYNCED state and available to send funds. Calling `sendToAddress` produces a flow of
* PendingTransaction objects which represent the active state of the transaction that was sent.
* Any time the state of that transaction changes, a new instance will be emitted.
*/
class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
private lateinit var synchronizer: Synchronizer
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() {
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
var seedPhrase = sharedViewModel.seedPhrase.value
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
// have the seed stored
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
runBlocking {
Initializer.new(requireApplicationContext()) {
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
}
}.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer)
}
spendingKey = runBlocking { DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }
}
//
// Observable properties (done without livedata or flows for simplicity)
//
private var balance: WalletBalance? = null
set(value) {
field = value
onUpdateSendButton()
}
private var isSending = false
set(value) {
field = value
if (value) Twig.sprout("Sending") else Twig.clip("Sending")
onUpdateSendButton()
}
private var isSyncing = true
set(value) {
field = value
onUpdateSendButton()
}
//
// Private functions
//
private fun initSendUi() {
amountInput = binding.inputAmount.apply {
setText(DemoConstants.sendAmount.toZecString())
}
addressInput = binding.inputAddress.apply {
setText(DemoConstants.toAddress)
}
binding.buttonSend.setOnClickListener(::onSend)
}
private fun monitorChanges() {
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
synchronizer.saplingBalances.collectWith(lifecycleScope, ::onBalance)
}
//
// Change listeners
//
private fun onStatus(status: Synchronizer.Status) {
binding.textStatus.text = "Status: $status"
isSyncing = status != Synchronizer.Status.SYNCED
if (status == Synchronizer.Status.SCANNING) {
binding.textBalance.text = "Calculating balance..."
} else {
if (!isSyncing) onBalance(balance)
}
}
private fun onProgress(i: Int) {
if (i < 100) {
binding.textStatus.text = "Downloading blocks...$i%"
binding.textBalance.visibility = View.INVISIBLE
} else {
binding.textBalance.visibility = View.VISIBLE
}
}
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
}
private fun onBalance(balance: WalletBalance?) {
this.balance = balance
if (!isSyncing) {
binding.textBalance.text = """
Available balance: ${balance?.available.convertZatoshiToZecString(12)}
Total balance: ${balance?.total.convertZatoshiToZecString(12)}
""".trimIndent()
}
}
private fun onSend(unused: View) {
isSending = true
val amount = amountInput.text.toString().toDouble().convertZecToZatoshi()
val toAddress = addressInput.text.toString().trim()
synchronizer.sendToAddress(
spendingKey,
amount,
toAddress,
"Funds from Demo App"
).collectWith(lifecycleScope, ::onPendingTxUpdated)
mainActivity()?.hideKeyboard()
}
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
val id = pendingTransaction?.id ?: -1
val message = when {
pendingTransaction == null -> "Transaction not found"
pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false }
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation..."
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false }
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false }
pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)"
pendingTransaction.isCreating() -> "Creating transaction!".also { onResetInfo() }
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
}
twig("Pending TX Updated: $message")
binding.textInfo.apply {
text = "$text\n$message"
}
}
private fun onUpdateSendButton() {
with(binding.buttonSend) {
when {
isSending -> {
text = "➡ sending"
isEnabled = false
}
isSyncing -> {
text = "⌛ syncing"
isEnabled = false
}
(balance?.available?.value ?: 0) <= 0 -> isEnabled = false
else -> {
text = "send"
isEnabled = true
}
}
}
}
private fun onResetInfo() {
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

@@ -0,0 +1,12 @@
@file:Suppress("ktlint:filename")
package cash.z.ecc.android.sdk.demoapp.ext
import android.content.Context
import androidx.fragment.app.Fragment
/**
* A safer alternative to [Fragment.requireContext], as it avoids leaking Fragment or Activity context
* when Application context is often sufficient.
*/
fun Fragment.requireApplicationContext(): Context = requireContext().applicationContext

View File

@@ -0,0 +1,40 @@
package cash.z.ecc.android.sdk.demoapp.util
import android.content.Context
import android.text.format.DateUtils
import androidx.fragment.app.Fragment
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
/**
* 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(context: Context) =
this?.let { timeInSeconds ->
DateUtils.getRelativeDateTimeString(
context,
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

@@ -0,0 +1,13 @@
@file:Suppress("ktlint:filename")
package cash.z.ecc.android.sdk.demoapp.util
import android.content.Context
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.type.ZcashNetwork
fun ZcashNetwork.Companion.fromResources(context: Context) = ZcashNetwork.valueOf(
context.getString(
R.string.network_name
)
)

View File

@@ -0,0 +1,62 @@
package cash.z.ecc.android.sdk.demoapp.util
import android.content.Context
@Deprecated(
message = "Do not use this! It is insecure and only intended for demo purposes to " +
"show how to bridge to an existing key storage mechanism. Instead, use the Android " +
"Keystore system or a 3rd party library that leverages it."
)
class SampleStorage(context: Context) {
private val prefs =
context.applicationContext.getSharedPreferences("ExtremelyInsecureStorage", Context.MODE_PRIVATE)
fun saveSensitiveString(key: String, value: String) {
prefs.edit().putString(key, value).apply()
}
fun loadSensitiveString(key: String): String? = prefs.getString(key, null)
fun saveSensitiveBytes(key: String, value: ByteArray) {
saveSensitiveString(key, value.toString(Charsets.ISO_8859_1))
}
fun loadSensitiveBytes(key: String): ByteArray? =
prefs.getString(key, null)?.toByteArray(Charsets.ISO_8859_1)
}
/**
* Simple demonstration of how to take existing code that securely stores data and bridge it into
* the SDK. This class delegates to the storage object. For demo purposes, we're using an insecure
* SampleStorage implementation but this can easily be swapped for a truly secure storage solution.
*/
class SampleStorageBridge(context: Context) {
private val delegate = SampleStorage(context.applicationContext)
/**
* Just a sugar method to help with being explicit in sample code. We want to show developers
* our intention that they write simple bridges to secure storage components.
*/
fun securelyStoreSeed(seed: ByteArray): SampleStorageBridge {
delegate.saveSensitiveBytes(KEY_SEED, seed)
return this
}
/**
* Just a sugar method to help with being explicit in sample code. We want to show developers
* our intention that they write simple bridges to secure storage components.
*/
fun securelyStorePrivateKey(key: String): SampleStorageBridge {
delegate.saveSensitiveString(KEY_PK, key)
return this
}
val seed: ByteArray get() = delegate.loadSensitiveBytes(KEY_SEED)!!
val key get() = delegate.loadSensitiveString(KEY_PK)!!
companion object {
private const val KEY_SEED = "cash.z.ecc.android.sdk.demoapp.SEED"
private const val KEY_PK = "cash.z.ecc.android.sdk.demoapp.PK"
}
}

View File

@@ -0,0 +1,27 @@
package cash.z.ecc.android.sdk.demoapp.util
import cash.z.android.plugin.MnemonicPlugin
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.bip39.toSeed
import java.util.Locale
/**
* A sample implementation of a plugin for handling Mnemonic phrases. Any library can easily be
* plugged into the SDK in this manner. In this case, we are wrapping a few example 3rd party
* libraries with a thin layer that converts from their API to ours via the MnemonicPlugin
* interface. We do not endorse these libraries, rather we just use them as an example of how to
* take existing infrastructure and plug it into the SDK.
*/
class SimpleMnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
}

View File

@@ -0,0 +1,31 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
android:width="108dp">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3L4.99,3c-1.11,0 -1.98,0.9 -1.98,2L3,19c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34 -3,-3L4.99,15L4.99,5L19,5v10zM16,10h-2L14,7h-4v3L8,10l4,4 4,-4z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z"/>
</vector>

View File

@@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
android:width="108dp">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/black"
android:pathData="M21,18v1c0,1.1 -0.9,2 -2,2L5,21c-1.11,0 -2,-0.9 -2,-2L3,5c0,-1.1 0.89,-2 2,-2h14c1.1,0 2,0.9 2,2v1h-9c-1.11,0 -2,0.9 -2,2v8c0,1.1 0.89,2 2,2h9zM12,16h10L22,8L12,8v8zM16,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,6h12v12H6z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M4,11h5L9,5L4,5v6zM4,18h5v-6L4,12v6zM10,18h5v-6h-5v6zM16,18h5v-6h-5v6zM10,11h5L15,5h-5v6zM16,5v6h5L21,5h-5z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7.77,6.76L6.23,5.48 0.82,12l5.41,6.52 1.54,-1.28L3.42,12l4.35,-5.24zM7,13h2v-2L7,11v2zM17,11h-2v2h2v-2zM11,13h2v-2h-2v2zM17.77,5.48l-1.54,1.28L20.58,12l-4.35,5.24 1.54,1.28L23.18,12l-5.41,-6.52z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,8l-4,4h3c0,3.31 -2.69,6 -6,6 -1.01,0 -1.97,-0.25 -2.8,-0.7l-1.46,1.46C8.97,19.54 10.43,20 12,20c4.42,0 8,-3.58 8,-8h3l-4,-4zM6,12c0,-3.31 2.69,-6 6,-6 1.01,0 1.97,0.25 2.8,0.7l1.46,-1.46C15.03,4.46 13.57,4 12,4c-4.42,0 -8,3.58 -8,8H1l4,4 4,-4H6z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M13,12h7v1.5h-7zM13,9.5h7L20,11h-7zM13,14.5h7L20,16h-7zM21,4L3,4c-1.1,0 -2,0.9 -2,2v13c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,6c0,-1.1 -0.9,-2 -2,-2zM21,19h-9L12,6h9v13z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF66BB6A" android:pathData="M20,5.41L18.59,4 7,15.59V9H5v10h10v-2H8.41z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFEF5350" android:pathData="M9,5v2h6.59L4,18.59 5.41,20 17,8.41V15h2V5z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:centerColor="#009688"
android:endColor="#00695C"
android:startColor="#4DB6AC"
android:type="linear" />
</shape>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/app_bar_main" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cash.z.ecc.android.sdk.demoapp.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@drawable/ic_floating_action" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/app_bar_main">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cash.z.ecc.android.sdk.demoapp.demos.gallery.GalleryFragment">
<TextView
android:id="@+id/text_gallery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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">
<TextView
android:id="@+id/text_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
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_constraintVertical_bias="0.2"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Initializing wallet..."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Balance"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/text_status"
app:layout_constraintHorizontal_bias="0.492"
app:layout_constraintStart_toStartOf="@+id/text_status"
app:layout_constraintTop_toBottomOf="@+id/text_status"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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">
<com.google.android.material.textfield.TextInputLayout
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_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
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: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>
<Button
android:id="@+id/button_apply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_start_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/text_layout_end_height"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
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"
android:text="500000"
android:textSize="20sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_end_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_apply"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/text_layout_start_height"
app:layout_constraintTop_toTopOf="parent"
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"
android:text="500000"
android:textSize="20sp" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_apply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:text="apply"
app:layout_constraintBottom_toBottomOf="@id/text_layout_end_height"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_layout_end_height"
app:layout_constraintTop_toTopOf="@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

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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">
<TextView
android:id="@+id/text_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="20sp"
android:text="loading latest height..."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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">
<EditText
android:id="@+id/text_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="18sp"
android:text="loading keys soon..."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cash.z.ecc.android.sdk.demoapp.demos.home.HomeFragment">
<TextView
android:id="@+id/text_seed_phrase"
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"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3" />
<!-- Input: Seed Phrase -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_seed_phrase"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Seed Phrase (24 words)"
app:endIconMode="clear_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.28"
app:layout_constraintWidth_percent="0.84"
tools:helperText="the seed phrase to use for this demo app">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_seed_phrase"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:imeActionLabel="use phrase"
android:imeOptions="actionDone"
android:inputType="textMultiLine|textNoSuggestions"
android:maxLines="9" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_paste"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Paste from Clipboard"
app:layout_constraintStart_toStartOf="@id/text_layout_seed_phrase"
app:layout_constraintTop_toBottomOf="@id/text_layout_seed_phrase" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_accept"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="@style/Widget.App.Button.OutlinedButton.IconOnly"
app:icon="@drawable/ic_baseline_check_24"
app:layout_constraintEnd_toEndOf="@id/text_layout_seed_phrase"
app:layout_constraintTop_toBottomOf="@id/text_layout_seed_phrase" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="@style/Widget.App.Button.OutlinedButton.IconOnly"
app:icon="@drawable/ic_baseline_close_24"
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"
android:layout_height="wrap_content"
android:text="\u25c4\tSelect a demo"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.1" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="text_layout_seed_phrase,button_paste,button_accept,button_cancel" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cash.z.ecc.android.sdk.demoapp.demos.home.HomeSecondFragment">
<TextView
android:id="@+id/textview_home_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@id/button_home_second"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_home_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/previous"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_home_second" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.88" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_start_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/text_layout_end_height"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.2" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_end_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
app:layout_constraintBottom_toBottomOf="parent"
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" />
<TextView
android:id="@+id/text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:padding="8dp"
android:text="Initializing..."
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<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_status" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_transactions"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_info"
app:layout_constraintBottom_toTopOf="@id/guideline_content_end"
tools:itemCount="15"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_transaction"
tools:orientation="vertical" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.88" />
<!-- Input: Address -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Address"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.025"
app:layout_constraintWidth_percent="0.90"
tools:helperText="the seed phrase to use for this demo app">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_address"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:inputType="textMultiLine|textNoSuggestions"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Input: Start -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_range_start"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Start height"
app:layout_constraintStart_toStartOf="@id/text_layout_address"
app:layout_constraintTop_toBottomOf="@id/text_layout_address"
app:layout_constraintWidth_percent="0.31">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_range_start"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:inputType="number"
android:maxLines="9" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Input: End -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_range_end"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:hint="End height"
app:layout_constraintStart_toEndOf="@id/text_layout_range_start"
app:layout_constraintTop_toTopOf="@id/text_layout_range_start"
app:layout_constraintWidth_percent="0.31">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_range_end"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:imeActionLabel="use phrase"
android:imeOptions="actionDone"
android:inputType="number"
android:maxLines="9" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/text_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:focusableInTouchMode="true"
android:gravity="center"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@id/button_load"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_layout_range_start"
tools:text="Initializing but this is a lot of text so what you gonna do about it" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_load"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="Load Transactions"
app:layout_constraintBottom_toBottomOf="@id/text_status"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/text_status" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_transactions"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="16dp"
app:layout_constraintBottom_toTopOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_status"
tools:itemCount="15"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_transaction"
tools:orientation="vertical" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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: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"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.05">
<com.google.android.material.textfield.TextInputEditText
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="18sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
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="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:maxLines="2"
android:ems="8"
android:hint="to address"
android:textSize="18sp" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:text="Send"
app:layout_constraintBottom_toBottomOf="@id/text_layout_amount"
app:layout_constraintStart_toEndOf="@id/text_layout_amount"/>
<TextView
android:id="@+id/text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="32dp"
android:text="Initializing wallet..."
app:layout_constraintStart_toStartOf="@id/text_layout_amount"
app:layout_constraintTop_toBottomOf="@id/text_layout_address" />
<TextView
android:id="@+id/text_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@id/text_status"
app:layout_constraintTop_toBottomOf="@id/text_status" />
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
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:paddingBottom="48dp"
android:paddingEnd="8dp" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cash.z.ecc.android.sdk.demoapp.demos.slideshow.SlideshowFragment">
<TextView
android:id="@+id/text_slideshow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#60F0F0F0"
android:elevation="1dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingEnd="10dp"
android:paddingStart="8dp"
android:layout_marginTop="4dp"
tools:ignore="RtlSymmetry">
<ImageView
android:id="@+id/image_transaction_type"
android:layout_width="40dp"
android:layout_height="40dp"
app:srcCompat="@drawable/ic_receive"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_transaction_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/text_transaction_info"
app:layout_constraintStart_toEndOf="@id/image_transaction_type"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="8/23 3:24pm" />
<TextView
android:id="@+id/text_transaction_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="12sp"
android:paddingEnd="16dp"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintEnd_toStartOf="@id/text_transaction_amount"
app:layout_constraintStart_toStartOf="@id/text_transaction_timestamp"
app:layout_constraintTop_toBottomOf="@id/text_transaction_timestamp"
app:layout_constraintBottom_toBottomOf="parent"
android:text="funds received" />
<TextView
android:id="@+id/text_transaction_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="+ 4.244"
android:textColor="@color/colorPrimary"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="@dimen/nav_header_height"
android:background="@drawable/side_nav_bar"
android:gravity="bottom"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/nav_header_desc"
android:paddingTop="@dimen/nav_header_vertical_spacing"
app:srcCompat="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:text="@string/nav_header_title"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nav_header_subtitle" />
</LinearLayout>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_home"
android:icon="@drawable/ic_menu_home"
android:title="@string/menu_home" />
<item
android:id="@+id/nav_private_key"
android:icon="@drawable/ic_menu_private_key"
android:title="@string/menu_private_key" />
<item
android:id="@+id/nav_address"
android:icon="@drawable/ic_menu_address"
android:title="@string/menu_address" />
<item
android:id="@+id/nav_balance"
android:icon="@drawable/ic_menu_balance"
android:title="@string/menu_balance" />
<item
android:id="@+id/nav_latest_height"
android:icon="@drawable/ic_menu_latest_height"
android:title="@string/menu_latest_height" />
<item
android:id="@+id/nav_block"
android:icon="@drawable/ic_menu_block"
android:title="@string/menu_block" />
<item
android:id="@+id/nav_block_range"
android:icon="@drawable/ic_menu_block_range"
android:title="@string/menu_block_range" />
<item
android:id="@+id/nav_transactions"
android:icon="@drawable/ic_list"
android:title="@string/menu_transactions" />
<item
android:id="@+id/nav_utxos"
android:icon="@drawable/ic_list"
android:title="@string/menu_utxos" />
<item
android:id="@+id/nav_send"
android:icon="@drawable/ic_menu_send"
android:title="@string/menu_send" />
</group>
</menu>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/nav_home">
<fragment
android:id="@+id/nav_home"
android:name="cash.z.ecc.android.sdk.demoapp.demos.home.HomeFragment"
android:label="@string/menu_home"
tools:layout="@layout/fragment_home">
</fragment>
<fragment
android:id="@+id/nav_address"
android:name="cash.z.ecc.android.sdk.demoapp.demos.getaddress.GetAddressFragment"
android:label="@string/menu_address"
tools:layout="@layout/fragment_get_address" />
<fragment
android:id="@+id/nav_balance"
android:name="cash.z.ecc.android.sdk.demoapp.demos.getbalance.GetBalanceFragment"
android:label="@string/menu_balance"
tools:layout="@layout/fragment_get_balance" />
<fragment
android:id="@+id/nav_private_key"
android:name="cash.z.ecc.android.sdk.demoapp.demos.getprivatekey.GetPrivateKeyFragment"
android:label="@string/menu_private_key"
tools:layout="@layout/fragment_get_private_key" />
<fragment
android:id="@+id/nav_latest_height"
android:name="cash.z.ecc.android.sdk.demoapp.demos.getlatestheight.GetLatestHeightFragment"
android:label="@string/menu_latest_height"
tools:layout="@layout/fragment_get_latest_height" />
<fragment
android:id="@+id/nav_block"
android:name="cash.z.ecc.android.sdk.demoapp.demos.getblock.GetBlockFragment"
android:label="@string/menu_block"
tools:layout="@layout/fragment_get_block" />
<fragment
android:id="@+id/nav_block_range"
android:name="cash.z.ecc.android.sdk.demoapp.demos.getblockrange.GetBlockRangeFragment"
android:label="@string/menu_block_range"
tools:layout="@layout/fragment_get_block_range" />
<fragment
android:id="@+id/nav_transactions"
android:name="cash.z.ecc.android.sdk.demoapp.demos.listtransactions.ListTransactionsFragment"
android:label="@string/menu_transactions"
tools:layout="@layout/fragment_list_transactions" />
<fragment
android:id="@+id/nav_utxos"
android:name="cash.z.ecc.android.sdk.demoapp.demos.listutxos.ListUtxosFragment"
android:label="@string/menu_utxos"
tools:layout="@layout/fragment_list_utxos" />
<fragment
android:id="@+id/nav_send"
android:name="cash.z.ecc.android.sdk.demoapp.demos.send.SendFragment"
android:label="@string/menu_send"
tools:layout="@layout/fragment_send" />
</navigation>

View File

@@ -0,0 +1,8 @@
<resources>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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

@@ -0,0 +1,8 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@@ -0,0 +1,28 @@
<resources>
<string name="app_name">Demo App</string>
<string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</string>
<string name="nav_header_subtitle">v1.1.0</string>
<string name="nav_header_desc">Navigation header</string>
<string name="action_settings">Change Seed Phrase</string>
<!-- Drawer Menu -->
<string name="menu_home">Home</string>
<string name="menu_private_key">Get Private Key</string>
<string name="menu_address">Get Address</string>
<string name="menu_balance">Get Balance</string>
<string name="menu_latest_height">Get Latest Height</string>
<string name="menu_block">Get Block</string>
<string name="menu_block_range">Get Block Range</string>
<string name="menu_decrypt">Decrypt Block</string>
<string name="menu_transactions">List Transactions</string>
<string name="menu_utxos">List UTXOs</string>
<string name="menu_send">Send</string>
<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

@@ -0,0 +1,29 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light" />
<style name="Widget.App.Button.OutlinedButton.IconOnly" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="iconPadding">0dp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:paddingLeft">12dp</item>
<item name="android:paddingRight">12dp</item>
<item name="android:minWidth">48dp</item>
<item name="android:minHeight">48dp</item>
</style>
</resources>

View File

@@ -0,0 +1,14 @@
package cash.z.ecc.android.sdk.demoapp
object DemoConstants {
val utxoEndHeight: Int = 968085
val sendAmount: Double = 0.000018
// corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx
val initialSeedWords: 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"
}

View File

@@ -0,0 +1,5 @@
<resources>
<string name="app_name">Mainnet Demo</string>
<string name="nav_header_title">Android SDK Demo : MAINNET</string>
<string name="network_name">Mainnet</string>
</resources>

View File

@@ -0,0 +1,14 @@
package cash.z.ecc.android.sdk.demoapp
object DemoConstants {
val utxoEndHeight: Int = 1075590
val sendAmount: Double = 0.00017
// corresponds to address: ztestsapling1zhqvuq8zdwa8nsnde7074kcfsat0w25n08jzuvz5skzcs6h9raxu898l48xwr8fmkny3zqqrgd9
val initialSeedWords: 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 =
"ztestsapling1ddttvrm6ueug4vwlczs8daqjaul60aur4udnvcz9qdnjt9ekt2tsxheqvv3mn50wvhmzj4ge9rl"
}

View File

@@ -0,0 +1,5 @@
<resources>
<string name="app_name">Testnet Demo</string>
<string name="nav_header_title">Android SDK Demo : TESTNET</string>
<string name="network_name">Testnet</string>
</resources>