Initial commit
@@ -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")
|
||||
}
|
||||
}
|
||||
27
demo-app/src/main/AndroidManifest.xml
Normal 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>
|
||||
18
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/App.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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}%"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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/>   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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class HomeViewModel : ViewModel()
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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> tx${it.index}:</b> ${it.spendsCount} / ${it.outputsCount}") }
|
||||
}
|
||||
} ?: ""
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
10
demo-app/src/main/res/drawable/ic_baseline_check_24.xml
Normal 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>
|
||||
10
demo-app/src/main/res/drawable/ic_baseline_close_24.xml
Normal 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>
|
||||
10
demo-app/src/main/res/drawable/ic_baseline_edit_24.xml
Normal 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>
|
||||
@@ -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>
|
||||
5
demo-app/src/main/res/drawable/ic_floating_action.xml
Normal 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>
|
||||
171
demo-app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_list.xml
Normal 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>
|
||||
10
demo-app/src/main/res/drawable/ic_menu_address.xml
Normal 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>
|
||||
10
demo-app/src/main/res/drawable/ic_menu_balance.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_menu_block.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_menu_block_range.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_menu_decrypt.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_menu_decrypt_range.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_menu_home.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_menu_latest_height.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_menu_memo.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_menu_private_key.xml
Normal 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>
|
||||
9
demo-app/src/main/res/drawable/ic_menu_send.xml
Normal 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>
|
||||
5
demo-app/src/main/res/drawable/ic_receive.xml
Normal 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>
|
||||
5
demo-app/src/main/res/drawable/ic_send.xml
Normal 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>
|
||||
10
demo-app/src/main/res/drawable/side_nav_bar.xml
Normal 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>
|
||||
26
demo-app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
34
demo-app/src/main/res/layout/app_bar_main.xml
Normal 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>
|
||||
21
demo-app/src/main/res/layout/content_main.xml
Normal 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>
|
||||
23
demo-app/src/main/res/layout/fragment_gallery.xml
Normal 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>
|
||||
21
demo-app/src/main/res/layout/fragment_get_address.xml
Normal 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>
|
||||
31
demo-app/src/main/res/layout/fragment_get_balance.xml
Normal 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>
|
||||
84
demo-app/src/main/res/layout/fragment_get_block.xml
Normal 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>
|
||||
82
demo-app/src/main/res/layout/fragment_get_block_range.xml
Normal 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>
|
||||
20
demo-app/src/main/res/layout/fragment_get_latest_height.xml
Normal 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>
|
||||
20
demo-app/src/main/res/layout/fragment_get_private_key.xml
Normal 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>
|
||||
116
demo-app/src/main/res/layout/fragment_home.xml
Normal 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>
|
||||
30
demo-app/src/main/res/layout/fragment_home_second.xml
Normal 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>
|
||||
75
demo-app/src/main/res/layout/fragment_list_transactions.xml
Normal 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>
|
||||
118
demo-app/src/main/res/layout/fragment_list_utxos.xml
Normal 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>
|
||||
94
demo-app/src/main/res/layout/fragment_send.xml
Normal 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>
|
||||
23
demo-app/src/main/res/layout/fragment_slideshow.xml
Normal 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>
|
||||
64
demo-app/src/main/res/layout/item_transaction.xml
Normal 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>
|
||||
37
demo-app/src/main/res/layout/nav_header_main.xml
Normal 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>
|
||||
49
demo-app/src/main/res/menu/activity_main_drawer.xml
Normal 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>
|
||||
10
demo-app/src/main/res/menu/main.xml
Normal 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>
|
||||
6
demo-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||
@@ -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>
|
||||
BIN
demo-app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
demo-app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
demo-app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
demo-app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demo-app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demo-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demo-app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
demo-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
demo-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
demo-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
60
demo-app/src/main/res/navigation/mobile_navigation.xml
Normal 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>
|
||||
8
demo-app/src/main/res/values-v21/styles.xml
Normal 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>
|
||||
9
demo-app/src/main/res/values/colors.xml
Normal 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>
|
||||
8
demo-app/src/main/res/values/dimens.xml
Normal 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>
|
||||
28
demo-app/src/main/res/values/strings.xml
Normal 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>
|
||||
29
demo-app/src/main/res/values/styles.xml
Normal 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>
|
||||
@@ -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"
|
||||
}
|
||||
5
demo-app/src/zcashmainnet/res/values/strings.xml
Normal 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>
|
||||
@@ -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"
|
||||
}
|
||||
5
demo-app/src/zcashtestnet/res/values/strings.xml
Normal 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>
|
||||