Merge pull request #163 from zcash/release/beta5

Release/beta5
This commit is contained in:
Kevin Gorham 2020-09-11 05:17:55 -04:00 committed by GitHub
commit 188381ef00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1797 additions and 635 deletions

View File

@ -1,6 +1,17 @@
Change Log
==========
Version 1.1.0-beta05 *(2020-09-11)*
------------------------------------
New: Synchronizer can now be started with just a viewing key.
New: Initializer improvments.
New: Added tool for loading checkpoints.
New: Added tool for deriving keys and addresses, statically.
New: Updated and revamped the demo apps.
New: Added a bit more (unofficial) t-addr support.
Fix: Broken testnet demo app.
Fix: Publishing configuration.
Version 1.1.0-beta04 *(2020-08-13)*
------------------------------------
New: Add support for canopy on testnet.

View File

@ -1,9 +1,6 @@
import cash.z.ecc.android.Deps
buildscript {
ext {
kotlin_version = '1.3.72'
}
ext.buildConfig = [
'compileSdkVersion': 29,
'minSdkVersion': 16,
@ -16,8 +13,7 @@ buildscript {
'paging': '2.1.2'
],
'grpc':'1.25.0', // NOTE: cannot use a higher version because they use protobuf 3.10+ which is not compatible with 3.0+ so we'd have to implement changes in our protobuf files which breaks everything
'kotlin': '1.3.72',
'coroutines': '1.3.5',
'coroutines': '1.3.9',
'junitJupiter': '5.6.1'
]
repositories {
@ -32,15 +28,15 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.kotlin:kotlin-allopen:${versions.kotlin}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Deps.kotlinVersion}"
classpath "org.jetbrains.kotlin:kotlin-allopen:${Deps.kotlinVersion}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.10.1"
classpath "com.github.ben-manes:gradle-versions-plugin:0.27.0"
classpath "com.github.ben-manes:gradle-versions-plugin:0.31.0"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.11"
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.13"
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:1.0.2'
classpath 'com.github.str4d:rust-android-gradle:68b4ecc053'
classpath 'org.owasp:dependency-check-gradle:5.3.0'
classpath 'org.owasp:dependency-check-gradle:6.0.0'
classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.5"
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
}
@ -89,7 +85,7 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
multiDexEnabled true
archivesBaseName = "zcash-android-wallet-sdk-${Deps.versionName}"
archivesBaseName = "zcash-android-sdk-${Deps.versionName}"
javaCompileOptions {
annotationProcessorOptions {
@ -214,7 +210,7 @@ cargo {
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
// Architecture Components: Lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:${versions.architectureComponents.lifecycle}"
@ -229,7 +225,7 @@ dependencies {
kapt "androidx.room:room-compiler:${versions.architectureComponents.room}"
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Deps.kotlinVersion}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}"
@ -248,8 +244,8 @@ dependencies {
// Tests
testImplementation 'androidx.multidex:multidex:2.0.1'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}"
testImplementation 'org.mockito:mockito-junit-jupiter:3.2.4'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${Deps.kotlinVersion}"
testImplementation 'org.mockito:mockito-junit-jupiter:3.5.10'
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
testImplementation "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}"
testImplementation "org.junit.jupiter:junit-jupiter-engine:${versions.junitJupiter}"
@ -261,12 +257,12 @@ dependencies {
// was buggy, crashing in several places. It also would require a separate test flavor because it's minimum API 26
// because "JUnit 5 uses Java 8-specific APIs that didn't exist on Android before the Oreo release."
androidTestImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
androidTestImplementation 'org.mockito:mockito-android:3.2.4'
androidTestImplementation "androidx.test:runner:1.2.0"
androidTestImplementation "androidx.test:core:1.2.0"
androidTestImplementation 'org.mockito:mockito-android:3.5.10'
androidTestImplementation "androidx.test:runner:1.3.0"
androidTestImplementation "androidx.test:core:1.3.0"
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:runner:1.3.0'
// sample mnemonic plugin
androidTestImplementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.1'

10
build.rs Normal file
View File

@ -0,0 +1,10 @@
use protobuf_codegen_pure;
fn main() {
protobuf_codegen_pure::Codegen::new()
.out_dir("src/main/rust")
.inputs(&["src/main/proto/local_rpc_types.proto"])
.includes(&["src/main/proto"])
.run()
.expect("Protobuf codegen failed");
}

View File

@ -3,11 +3,11 @@ package cash.z.ecc.android
object Deps {
// For use in the top-level build.gradle which gives an error when provided
// `Deps.Kotlin.version` directly
const val kotlinVersion = "1.3.72"
const val kotlinVersion = "1.4.0"
const val group = "cash.z.ecc.android"
const val artifactName = "zcash-android-wallet-sdk"
const val versionName = "1.1.0-beta04"
const val versionCode = 1_01_00_204 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
const val artifactName = "zcash-android-sdk"
const val versionName = "1.1.0-beta05"
const val versionCode = 1_01_00_205 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
const val description = "This lightweight SDK connects Android to Zcash. It welds together Rust and Kotlin in a minimal way, allowing third-party Android apps to send and receive shielded transactions easily, securely and privately."
const val githubUrl = "https://github.com/zcash/zcash-android-wallet-sdk"
@ -15,17 +15,17 @@ object Deps {
// NOTE: to upload run: ./gradlew bintrayUpload after setting BINTRAY_USER and BINTRAY_API_KEY as environment variable
// to publish for local development run: ./gradlew publishToMavenLocal
// Remember: publish both mainnet and testnet!
const val publishingDryRun = true
val publishingTarget = Publication.Mainnet
const val publishingDryRun = false
val publishingTarget = Publication.Testnet
object Publication {
object Mainnet {
const val variant = "zcashmainnetRelease"
const val artifactId = "sdk-mainnet"
const val artifactId = "zcash-android-sdk-mainnet"
}
object Testnet {
const val variant = "zcashtestnetRelease"
const val artifactId = "sdk-testnet"
const val artifactId = "zcash-android-sdk-testnet"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.StateFlow
class SharedViewModel : ViewModel() {
private val config = App.instance.defaultConfig
private val _seedPhrase = MutableStateFlow(config.seedWords)
private val _seedPhrase = MutableStateFlow(config.initialSeedWords)
// publicly, this is read-only
val seedPhrase: StateFlow<String> get() = _seedPhrase

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,12 +24,20 @@ import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.rpc.LocalRpcTypes
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
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.
@ -40,23 +48,21 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
private val config = App.instance.defaultConfig
private val initializer =
Initializer(App.instance, host = config.host, port = config.port, alias = "Demo_Utxos")
private val birthday = config.loadBirthday()
private val birthday = WalletBirthdayTool.loadNearest(App.instance, config.birthdayHeight)
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
val latestBlockHeight = 935000
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListUtxosBinding =
FragmentListUtxosBinding.inflate(layoutInflater)
fun initUi() {
binding.inputAddress.setText(address)
binding.inputRangeStart.setText(ZcashSdk.SAPLING_ACTIVATION_HEIGHT.toString())
binding.inputRangeEnd.setText(latestBlockHeight.toString())
binding.inputRangeEnd.setText(config.utxoEndHeight.toString())
binding.buttonLoad.setOnClickListener {
mainActivity()?.hideKeyboard()
@ -66,8 +72,6 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
initTransactionUi()
}
private lateinit var lightwalletService: LightWalletService
fun downloadTransactions() {
binding.textStatus.text = "loading..."
@ -75,15 +79,14 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
binding.textStatus.requestFocus()
val addressToUse = binding.inputAddress.text.toString()
val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashSdk.SAPLING_ACTIVATION_HEIGHT
val endToUse = binding.inputRangeEnd.text.toString().toIntOrNull() ?: latestBlockHeight
lightwalletService = LightWalletGrpcService(App.instance, config.host, config.port)
val endToUse = binding.inputRangeEnd.text.toString().toIntOrNull() ?: config.utxoEndHeight
var allStart = now
twig("loading transactions in range $startToUse..$endToUse")
val txids = lightwalletService.getTAddressTransactions(addressToUse, startToUse..endToUse)
val txids = lightwalletService?.getTAddressTransactions(addressToUse, startToUse..endToUse)
var delta = now - allStart
updateStatus("found ${txids.size} transactions in ${delta}ms.", false)
updateStatus("found ${txids?.size} transactions in ${delta}ms.", false)
txids.map {
txids?.map {
it.data.apply {
try {
initializer.rustBackend.decryptAndStoreTransaction(toByteArray())
@ -91,7 +94,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
twig("failed to decrypt and store transaction due to: $t")
}
}
}.let { txData ->
}?.let { txData ->
val parseStart = now
val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build()
val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList)
@ -136,7 +139,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
super.onResume()
resetInBackground()
val seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
binding.inputAddress.setText(initializer.rustBackend.deriveTAddress(seed))
binding.inputAddress.setText(DerivationTool.deriveTransparentAddress(seed))
}
@ -226,13 +229,6 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
private fun onTransactionsUpdated(transactions: PagedList<ConfirmedTransaction>) {
twig("got a new paged list of transactions of size ${transactions.size}")
adapter.submitList(transactions)
// show message when there are no transactions
// if (isSynced) {
if (transactions.isEmpty()) {
Toast.makeText(activity, "No transactions found. Try another address or send funds to this one.", Toast.LENGTH_LONG).show()
}
// }
}
override fun onActionButtonClicked() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,19 @@
package cash.z.ecc.android.sdk.demoapp
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.demoapp.util.SimpleMnemonics
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
data class DemoConfig(
val alias: String = "SdkDemo",
val host: String = "lightwalletd.electriccoin.co",
val port: Int = 9067,
val birthdayHeight: Int = 835_000,
val sendAmount: Double = 0.0018,
val birthdayHeight: Int = 968000,
val utxoEndHeight: Int = 968085,
val sendAmount: Double = 0.000018,
// corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx
val seedWords: String = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame",
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"
) {
val seed: ByteArray get() = SimpleMnemonics().toSeed(seedWords.toCharArray())
fun newWalletBirthday() = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance)
fun loadBirthday(height: Int = birthdayHeight) = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance, height)
}
)

View File

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

View File

@ -1,22 +1,16 @@
package cash.z.ecc.android.sdk.demoapp
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.demoapp.util.SimpleMnemonics
data class DemoConfig(
val host: String = "lightwalletd.testnet.z.cash",
val alias: String = "SdkDemo",
val host: String = "lightwalletd.testnet.electriccoin.co",
val port: Int = 9067,
val birthdayHeight: Int = 820_000,
val sendAmount: Double = 0.0018,
val birthdayHeight: Int = 954_500,
val utxoEndHeight: Int = 1075590,
val sendAmount: Double = 0.00017,
// corresponds to address: ztestsapling1zhqvuq8zdwa8nsnde7074kcfsat0w25n08jzuvz5skzcs6h9raxu898l48xwr8fmkny3zqqrgd9
val seedWords: String = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame",
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"
) {
val seed: ByteArray get() = SimpleMnemonics().toSeed(seedWords.toCharArray())
fun newWalletBirthday() = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance)
fun loadBirthday(height: Int = birthdayHeight) = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance, height)
}
)

View File

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

View File

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

View File

@ -1 +1 @@
rootProject.name = 'zcash-android-wallet-sdk'
rootProject.name = 'zcash-android-sdk'

View File

@ -0,0 +1,31 @@
package cash.z.ecc.android.sdk
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import org.junit.Assert.assertEquals
import org.junit.Test
class VkInitializerTest {
@Test
fun testInit() {
val height = 1_419_900
val initializer = VkInitializer(context) {
importedWalletBirthday(height)
viewingKeys("zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
"zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7")
alias = "VkInitTest2"
}
assertEquals(height, initializer.birthday.height)
}
companion object {
private val context = InstrumentationRegistry.getInstrumentation().context
init {
Twig.plant(TroubleshootingTwig())
}
}
}

View File

@ -0,0 +1,88 @@
//package cash.z.ecc.android.sdk.integration
//
//import cash.z.ecc.android.sdk.ext.ScopedTest
//import cash.z.ecc.android.sdk.ext.twigTask
//import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
//import kotlinx.coroutines.runBlocking
//import org.junit.BeforeClass
//import org.junit.Test
//
//class MultiAccountIntegrationTest : ScopedTest() {
//
// /**
// * Test multiple viewing keys by doing the following:
// *
// * - sync "account A" with 100 test blocks containing:
// * (in zatoshi) four 100_000 notes and one 10_000 note
// * - import a viewing key for "account B"
// * - send a 10_000 zatoshi transaction from A to B
// * - include that tx in the next block and mine that block (on the darkside), then scan it
// * - verify that A's balance reflects a single 100_000 note being spent but pending confirmations
// * - advance the chain by 9 more blocks to reach 10 confirmations
// * - verify that the change from the spent note is reflected in A's balance
// * - check B's balance and verify that it received the full 10_000 (i.e. that A paid the mining fee)
// *
// * Although we sent funds to an address, the synchronizer has both spending keys so it is able
// * to track transactions for both addresses!
// */
// @Test
// fun testViewingKeyImport() = runBlocking {
// validatePreConditions()
//
// with(sithLord) {
// twigTask("importing viewing key") {
//// synchronizer.importViewingKey(secondKey)
// }
//
// twigTask("Sending funds") {
// sithLord.createAndSubmitTx(10_000, secondAddress, "multi-account works!")
// chainMaker.applyPendingTransactions(663251)
// await(targetHeight = 663251)
// }
// // verify that the transaction block height was scanned
// validator.validateMinHeightScanned(663251)
//
// // balance before confirmations (the large 100_000 note gets selected)
// validator.validateBalance(310_000)
//
// // add remaining confirmations so that funds become spendable and await until they're scanned
// chainMaker.advanceBy(9)
// await(targetHeight = 663260)
//
// // balance after confirmations
// validator.validateBalance(390_000)
//
// // check the extra viewing key balance!!!
// // accountIndex 1 corresponds to the imported viewingKey for the address where we sent the funds!
// validator.validateBalance(available = 10_000, accountIndex = 1)
// }
// }
//
// /**
// * Verify that before the integration test begins, the wallet is synced up to the expected block
// * and contains the expected balance.
// */
// private fun validatePreConditions() {
// with(sithLord) {
// twigTask("validating preconditions") {
// validator.validateMinHeightScanned(663250)
// validator.validateMinBalance(410_000)
// }
// }
// }
//
//
// companion object {
// private val sithLord = DarksideTestCoordinator("192.168.1.134")
// private val secondAddress = "zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx"
// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord.enterTheDarkside()
// sithLord.chainMaker.makeSimpleChain()
// sithLord.startSync(classScope).await()
// }
// }
//}

View File

@ -0,0 +1,75 @@
package cash.z.ecc.android.sdk.integration
//import cash.z.ecc.android.sdk.SdkSynchronizer
//import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
//import cash.z.ecc.android.sdk.ext.ScopedTest
//import cash.z.ecc.android.sdk.ext.twig
//import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
//import kotlinx.coroutines.Job
//import kotlinx.coroutines.delay
//import kotlinx.coroutines.flow.launchIn
//import kotlinx.coroutines.flow.onEach
//import kotlinx.coroutines.runBlocking
//import org.junit.Assert.assertEquals
//import org.junit.BeforeClass
//import org.junit.Test
//class MultiAccountTest : ScopedTest() {
//
// @Test
// fun testTargetBlock_sanityCheck() {
// with(sithLord) {
// validator.validateMinHeightScanned(663250)
// validator.validateMinBalance(200000)
// }
// }
//
// @Test
// fun testTargetBlock_send() = runBlocking {
// with(sithLord) {
//
// twig("<importing viewing key><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>")
// synchronizer.importViewingKey(secondKey)
// twig("<DONE importing viewing key><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>")
//
// twig("IM GONNA SEND!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
// sithLord.sendAndWait(testScope, spendingKey, 10000, secondAddress, "multi-account works!")
// chainMaker.applySentTransactions()
// await(targetHeight = 663251)
//
// twig("done waiting for 663251!")
// validator.validateMinHeightScanned(663251)
//
// // balance before confirmations
// validator.validateBalance(310000)
//
// // add remaining confirmations
// chainMaker.advanceBy(9)
// await(targetHeight = 663260)
//
// // balance after confirmations
// validator.validateBalance(390000)
//
// // check the extra viewing key balance!!!
// val account1Balance = (synchronizer as SdkSynchronizer).processor.getBalanceInfo(1)
// assertEquals(10000, account1Balance.totalZatoshi)
// twig("done waiting for 663261!")
// }
// }
//
//
// companion object {
// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
// private val sithLord = DarksideTestCoordinator("192.168.1.134")
// private val secondAddress = "zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx"
// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord.enterTheDarkside()
// sithLord.chainMaker.simpleChain()
// sithLord.startSync(classScope).await()
// }
// }
//}

View File

@ -0,0 +1,196 @@
package cash.z.ecc.android.sdk.integration
import cash.z.ecc.android.sdk.ext.ScopedTest
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.ext.twigTask
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.util.DarksideTestCoordinator
import cash.z.ecc.android.sdk.util.SimpleMnemonics
import cash.z.wallet.sdk.rpc.CompactFormats
import cash.z.wallet.sdk.rpc.Service
import io.grpc.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import java.util.concurrent.TimeUnit
//class MultiRecipientIntegrationTest : ScopedTest() {
//
// @Test
// @Ignore
// fun testMultiRecipients() = runBlocking {
// with(sithLord) {
// val m = SimpleMnemonics()
// randomPhrases.map {
// m.toSeed(it.toCharArray())
// }.forEach { seed ->
// twig("ZyZ4: I've got a seed $seed")
// initializer.apply {
//// delay(250)
// twig("VKZyZ: ${deriveViewingKeys(seed)[0]}")
//// delay(500)
// twig("SKZyZ: ${deriveSpendingKeys(seed)[0]}")
//// delay(500)
// twig("ADDRZyZ: ${deriveAddress(seed)}")
//// delay(250)
// }
// }
// }
// delay(500)
// }
//
// @Test
// fun loadVks() = runBlocking {
// with(sithLord) {
// viewingKeys.forEach {
// twigTask("importing viewing key") {
// synchronizer.importViewingKey(it)
// }
// }
// twigTask("Sending funds") {
// createAndSubmitTx(10_000, addresses[0], "multi-account works!")
// chainMaker.applyPendingTransactions(663251)
// await(targetHeight = 663251)
// }
// }
// }
//
//// private fun sendToMyHomies() {
//// twig("uno")
//// val rustPoc = LightWalletGrpcService(localChannel)
//// twig("dos")
//// val pong: Int = rustPoc.getLatestBlockHeight()
//// twig("tres")
//// assertEquals(800000, pong)
//// }
//
//
// private fun sendToMyHomies0() {
// val rustPoc = LocalWalletGrpcService(localChannel)
// val pong: Service.PingResponse = rustPoc.sendMoney(Service.PingResponse.newBuilder().setEntry(10).setEntry(11).build())
// assertEquals(pong.entry, 12)
// }
//
// object localChannel : ManagedChannel() {
// private var _isShutdown = false
// get() {
// twig("zyz: returning _isShutdown")
// return field
// }
// private var _isTerminated = false
// get() {
// twig("zyz: returning _isTerminated")
// return field
// }
//
// override fun <RequestT : Any?, ResponseT : Any?> newCall(
// methodDescriptor: MethodDescriptor<RequestT, ResponseT>?,
// callOptions: CallOptions?
// ): ClientCall<RequestT, ResponseT> {
// twig("zyz: newCall")
// return LocalCall()
// }
//
// override fun isTerminated() = _isTerminated
//
// override fun authority(): String {
// twig("zyz: authority")
// return "none"
// }
//
// override fun shutdown(): ManagedChannel {
// twig("zyz: shutdown")
// _isShutdown = true
// return this
// }
//
// override fun isShutdown() = _isShutdown
//
// override fun shutdownNow() = shutdown()
//
// override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean {
// twig("zyz: awaitTermination")
// _isTerminated = true
// return _isTerminated
// }
// }
//
// class LocalCall<RequestT, ResponseT> : ClientCall<RequestT, ResponseT>() {
// override fun sendMessage(message: RequestT) {
// twig("zyz: sendMessage: $message")
// }
//
// override fun halfClose() {
// twig("zyz: halfClose")
// }
//
// override fun start(responseListener: Listener<ResponseT>?, headers: Metadata?) {
// twig("zyz: start")
// responseListener?.onMessage(Service.BlockID.newBuilder().setHeight(800000).build() as? ResponseT)
// responseListener?.onClose(Status.OK, headers)
// }
//
// override fun cancel(message: String?, cause: Throwable?) {
// twig("zyz: cancel: $message caused by $cause")
// }
//
// override fun request(numMessages: Int) {
// twig("zyz: request $numMessages")
// }
// }
//
// private fun sendToMyHomies1() = runBlocking {
// with(sithLord) {
// twigTask("Sending funds") {
//// createAndSubmitTx(200_000, addresses[0], "multi-account works!")
// chainMaker.applyPendingTransactions(663251)
// await(targetHeight = 663251)
// }
// }
// }
//
// companion object {
// private val sithLord = DarksideTestCoordinator("192.168.1.134", "MultiRecipientInRust")
//
// private val randomPhrases = listOf(
// "profit save black expose rude feature early rocket alter borrow finish october few duty flush kick spell bean burden enforce bitter theme silent uphold",
// "unit ice dial annual duty feature smoke expose hard joy globe just accuse inner fog cash neutral forum strategy crash subject hurdle lecture sand",
// "average talent frozen work brand output major soldier witness keen brown bind indicate burden furnace long crime joke inhale chronic ordinary renew boat flame",
// "echo viable panic unaware stay magnet cake museum yellow abandon mountain height lunch advance tongue market bamboo cushion okay morning minute icon obtain december",
// "renew enlist travel stand trust execute decade surge follow push student school focus woman ripple movie that bitter plug same index wife spread differ"
// )
//
// private val viewingKeys = listOf(
// "zxviews1qws7ryw7qqqqpqq77dmhl9tufzdsgy8hcjq8kxjtgkfwwgqn4a26ahmhmjqueptd2pmq3f73pm8uaa25aze5032qw4dppkx4l625xcjcm94d5e65fcq4j2uptnjuqpyu2rvud88dtjwseglgzfe5l4te2xw62yq4tv62d2f6kl4706c6dmfxg2cmsdlzlt9ykpvacaterq4alljr3efke7k46xcrg4pxc02ezj0txwqjjve23nqqp7t5n5qat4d8569krxgkcd852uqg2t2vn",
// "zxviews1qdtp7dwfqqqqpqq3zxegnzc6qtacjp4m6qhyz7typdw9h9smra3rn322dkhyfg8kktk66k7zaj9tt5j6e58enx89pwry4rxwmcuzqyxlsap965r5gxpt604chmjyuhder6xwu3tx0h608as5sgxapqdqa6v6hy6qzh9fft0ns3cj9f8zrhu0ukzf9gn2arr02kzdct0jh5ee3zjch3xscjv34pzkgpueuq0pyl706alssuchqu4jmjm22fcq3htlwxt3f3hdytne7mgscrz5m",
// "zxviews1qvfmgpzjqqqqpqqnpl2s9n774mrv72zsuw73km9x6ax2s26d0d0ua20nuxvkexa4lq5fsc6psl8csspyqrlwfeuele5crlwpyjufgkzyy6ffw8hc52hn04jzru6mntms8c2cm255gu200zx4pmz06k3s90jatwehazl465tf6uyj6whwarpcca9exzr7wzltelq5tusn3x3jchjyk6cj09xyctjzykp902w4x23zdsf46d3fn9rtkgm0rmek296c5nhuzf99a2x6umqr804k9",
// "zxviews1qv85jn3hqqqqpq9jam3g232ylvvhy8e5vdhp0x9zjppr49sw6awwrm3a3d8l9j9es2ed9h29r6ta5tzt53j2y0ex84lzns0thp7n9wzutjapq29chfewqz34q5g6545f8jf0e69jcg9eyv66s8pt3y5dwxg9nrezz8q9j9fwxryeleayay6m09zpt0dem8hkazlw5jk6gedrakp9z7wzq2ptf6aqkft6z02mtrnq4a5pguwp4m8xkh52wz0r3naeycnqllnvsn8ag5q73pqgd",
// "zxviews1qwhel8pxqqqqpqxjl3cqu2z8hu0tqdd5qchkrdtsjuce9egdqlpu7eff2rn3gknm0msw7ug6qp4ynppscvv6hfm2nkf42lhz8la5et3zsej84xafcn0xdd9ms452hfjp4tljshtffscsl68wgdv3j5nnelxsdcle5rnwkuz6lvvpqs7s2x0cnhemhnwzhx5ccakfgxfym0w8dxglq4h6pwukf2az6lcm38346qc5s9rgx6s988fr0kxnqg0c6g6zlxa2wpc7jh0gz7q4ysx0l"
// )
// private val spendingKeys = listOf(
// "secret-extended-key-main1qws7ryw7qqqqpqq77dmhl9tufzdsgy8hcjq8kxjtgkfwwgqn4a26ahmhmjqueptd2pt49qhm63lt8v93tlqzw7psmkvqqfm6xdnc2qwkflfcenqs7s4sj2yn0c75n982wjrf5k5h37vt3wxwr3pqnjk426lltctrms2uqmqgkl4706c6dmfxg2cmsdlzlt9ykpvacaterq4alljr3efke7k46xcrg4pxc02ezj0txwqjjve23nqqp7t5n5qat4d8569krxgkcd852uqxj5ljt",
// "secret-extended-key-main1qdtp7dwfqqqqpqq3zxegnzc6qtacjp4m6qhyz7typdw9h9smra3rn322dkhyfg8kk26p0fcjuklryw0ed6falf6c7dwqehleca0xf6m6tlnv5zdjx7lqs4xmseqjz0fvk273aczatxxjaqmy3kv8wtzcc6pf6qtrjy5g2mqgs3cj9f8zrhu0ukzf9gn2arr02kzdct0jh5ee3zjch3xscjv34pzkgpueuq0pyl706alssuchqu4jmjm22fcq3htlwxt3f3hdytne7mgacmaq6",
// "secret-extended-key-main1qvfmgpzjqqqqpqqnpl2s9n774mrv72zsuw73km9x6ax2s26d0d0ua20nuxvkexa4lzc4n8a3zfvyn2qns37fx00avdtjewghmxz5nc2ey738nrpu4pqqnwysmcls5yek94lf03d5jtsa25nmuln4xjvu6e4g0yrr6xesp9cr6uyj6whwarpcca9exzr7wzltelq5tusn3x3jchjyk6cj09xyctjzykp902w4x23zdsf46d3fn9rtkgm0rmek296c5nhuzf99a2x6umqvf4man",
// "secret-extended-key-main1qv85jn3hqqqqpq9jam3g232ylvvhy8e5vdhp0x9zjppr49sw6awwrm3a3d8l9j9estq9a548lguf0n9fsjs7c96uaymhysuzeek5eg8un0fk8umxszxstm0xfq77x68yjk4t4j7h2xqqjf8nmkx0va3cphnhxpvd0l5dhzgyxryeleayay6m09zpt0dem8hkazlw5jk6gedrakp9z7wzq2ptf6aqkft6z02mtrnq4a5pguwp4m8xkh52wz0r3naeycnqllnvsn8ag5qru36vk",
// "secret-extended-key-main1qwhel8pxqqqqpqxjl3cqu2z8hu0tqdd5qchkrdtsjuce9egdqlpu7eff2rn3gknm0mdwr9358t3dlcf47vakdwewxy64k7ds7y3k455rfch7s2x8mfesjsxptyfvc9heme3zj08wwdk4l9mwce92lvrl797wmmddt65ygwcqlvvpqs7s2x0cnhemhnwzhx5ccakfgxfym0w8dxglq4h6pwukf2az6lcm38346qc5s9rgx6s988fr0kxnqg0c6g6zlxa2wpc7jh0gz7qx7zl33"
// )
// private val addresses = listOf(
// "zs1d8lenyz7uznnna6ttmj6rk9l266989f78c3d79f0r6r28hn0gc9fzdktrdnngpcj8wr2cd4zcq2",
// "zs13x79khp5z0ydgnfue8p88fjnrjxtnz0gwxyef525gd77p72nqh7zr447n6klgr5yexzp64nc7hf",
// "zs1jgvqpsyzs90hlqz85qry3zv52keejgx0f4pnljes8h4zs96zcxldu9llc03dvhkp6ds67l4s0d5",
// "zs1lr428hhedq3yk8n2wr378e6ua3u3r4ma5a8dqmf3r64y96vww5vh6327jfudtyt7v3eqw22c2t6",
// "zs1hy7mdwl6y0hwxts6a5lca2xzlr0p8v5tkvvz7jfa4d04lx5uedg6ya8fmthywujacx0acvfn837"
// )
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord.enterTheDarkside()
// sithLord.chainMaker.makeSimpleChain()
// sithLord.startSync(classScope).await()
// }
// }
//}

View File

@ -0,0 +1,59 @@
package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import org.junit.Ignore
import org.junit.Test
class TransactionCounterUtil {
private val context = InstrumentationRegistry.getInstrumentation().context
private val service = LightWalletGrpcService(context, ZcashSdk.DEFAULT_LIGHTWALLETD_HOST, ZcashSdk.DEFAULT_LIGHTWALLETD_PORT)
init {
Twig.plant(TroubleshootingTwig())
}
@Test
@Ignore
fun testBlockSize() {
val sizes = mutableMapOf<Int, Int>()
service.getBlockRange(900_000..910_000).forEach { b ->
twig("h: ${b.header.size()}")
val s = b.serializedSize
sizes[s] = (sizes[s] ?: 0) + 1
}
twig("sizes: ${sizes.toSortedMap()}")
}
@Test
@Ignore
fun testCountTransactions() {
val txCounts = mutableMapOf<Int, Int>()
val outputCounts = mutableMapOf<Int, Int>()
var totalOutputs = 0
var totalTxs = 0
service.getBlockRange(900_000..950_000).forEach { b ->
b.header.size()
b.vtxList.map { it.outputsCount }.forEach { oCount ->
outputCounts[oCount] = (outputCounts[oCount] ?: 0) + oCount.coerceAtLeast(1)
totalOutputs += oCount
}
b.vtxCount.let { count ->
txCounts[count] = (txCounts[count] ?: 0) + count.coerceAtLeast(1)
totalTxs += count
}
}
twig("txs: $txCounts")
twig("outputs: $outputCounts")
twig("total: $totalTxs $totalOutputs")
}
}
/*
*/

View File

@ -14,6 +14,8 @@ import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool.WalletBirthday
import cash.z.ecc.android.sdk.transaction.*
import com.google.gson.Gson
import com.google.gson.stream.JsonReader
@ -41,11 +43,11 @@ import kotlin.reflect.KProperty
*/
class Initializer(
appContext: Context,
val host: String = ZcashSdk.DEFAULT_LIGHTWALLETD_HOST,
val port: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT,
private val alias: String = ZcashSdk.DEFAULT_ALIAS
) {
val context = appContext.applicationContext
override val host: String = ZcashSdk.DEFAULT_LIGHTWALLETD_HOST,
override val port: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT,
override val alias: String = ZcashSdk.DEFAULT_ALIAS
) : SdkSynchronizer.SdkInitializer {
override val context = appContext.applicationContext
init {
validateAlias(alias)
@ -78,7 +80,7 @@ class Initializer(
* SDK when it is constructed. It provides access to all Librustzcash features and is configured
* based on this initializer.
*/
val rustBackend: RustBackend get() {
override val rustBackend: RustBackend get() {
check(_rustBackend != null) {
"Error: RustBackend must be loaded before it is accessed. Verify that either" +
" the 'open', 'new' or 'import' function has been called on the Initializer."
@ -179,7 +181,7 @@ class Initializer(
*/
fun open(birthday: WalletBirthday): Initializer {
twig("Opening wallet with birthday ${birthday.height}")
requireRustBackend().birthdayHeight = birthday.height
requireRustBackend(birthday)
return this
}
@ -222,16 +224,16 @@ class Initializer(
this.birthday = birthday
twig("Initializing accounts with birthday ${birthday.height}")
try {
requireRustBackend().clear(clearCacheDb, clearDataDb)
requireRustBackend(birthday).clear(clearCacheDb, clearDataDb)
// only creates tables, if they don't exist
requireRustBackend().initDataDb()
requireRustBackend(birthday).initDataDb()
twig("Initialized wallet for first run")
} catch (t: Throwable) {
throw InitializerException.FalseStart(t)
}
try {
requireRustBackend().initBlocksTable(
requireRustBackend(birthday).initBlocksTable(
birthday.height,
birthday.hash,
birthday.time,
@ -247,7 +249,7 @@ class Initializer(
}
try {
return requireRustBackend().initAccountsTable(seed, numberOfAccounts).also {
return requireRustBackend(birthday).initAccountsTable(seed, numberOfAccounts).also {
twig("Initialized the accounts table with ${numberOfAccounts} account(s)")
}
} catch (t: Throwable) {
@ -259,7 +261,7 @@ class Initializer(
* Delete all local data related to this wallet, as though the wallet was never created on this
* device. Simply put, this call deletes the "cache db" and "data db."
*/
fun clear() {
override fun clear() {
rustBackend.clear()
}
@ -270,78 +272,14 @@ class Initializer(
*
* @return the rustBackend that was loaded by this initializer.
*/
private fun requireRustBackend(): RustBackend {
private fun requireRustBackend(walletBirthday: WalletBirthday? = null): RustBackend {
if (!isInitialized) {
twig("Initializing cache: $pathCacheDb data: $pathDataDb params: $pathParams")
_rustBackend = RustBackend().init(pathCacheDb, pathDataDb, pathParams)
_rustBackend = RustBackend().init(pathCacheDb, pathDataDb, pathParams, walletBirthday?.height)
}
return rustBackend
}
//
// Key Derivation Helpers
//
/**
* Given a seed and a number of accounts, return the associated spending keys. These keys can
* be used to derive the viewing keys.
*
* @param seed the seed from which to derive spending keys.
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully
* supported so the default value of 1 is recommended.
*
* @return the spending keys that correspond to the seed, formatted as Strings.
*/
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String> =
requireRustBackend().deriveSpendingKeys(seed, numberOfAccounts)
/**
* Given a seed and a number of accounts, return the associated viewing keys.
*
* @param seed the seed from which to derive viewing keys.
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully
* supported so the default value of 1 is recommended.
*
* @return the viewing keys that correspond to the seed, formatted as Strings.
*/
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String> =
requireRustBackend().deriveViewingKeys(seed, numberOfAccounts)
/**
* Given a spending key, return the associated viewing key.
*
* @param spendingKey the key from which to derive the viewing key.
*
* @return the viewing key that corresponds to the spending key.
*/
fun deriveViewingKey(spendingKey: String): String =
requireRustBackend().deriveViewingKey(spendingKey)
/**
* Given a seed and account index, return the associated address.
*
* @param seed the seed from which to derive the address.
* @param accountIndex the index of the account to use for deriving the address. Multiple
* accounts are not fully supported so the default value of 1 is recommended.
*
* @return the address that corresponds to the seed and account index.
*/
fun deriveAddress(seed: ByteArray, accountIndex: Int = 0) =
requireRustBackend().deriveAddress(seed, accountIndex)
/**
* Given a viewing key string, return the associated address.
*
* @param viewingKey the viewing key to use for deriving the address. The viewing key is tied to
* a specific account so no account index is required.
*
* @return the address that corresponds to the viewing key.
*/
fun deriveAddress(viewingKey: String) =
requireRustBackend().deriveAddress(viewingKey)
companion object {
//
@ -374,22 +312,6 @@ class Initializer(
}
}
/**
* Model object for holding a wallet birthday. It is only used by this class.
*
* @param height the height at the time the wallet was born.
* @param hash the hash of the block at the height.
* @param time the block time at the height.
* @param tree the sapling tree corresponding to the height.
*/
data class WalletBirthday(
val height: Int = -1,
val hash: String = "",
val time: Long = -1,
val tree: String = ""
)
/**
* Interface for classes that can handle birthday storage. This makes it possible to bridge into
* existing storage logic. Instances of this interface can also be used as property delegates,
@ -461,7 +383,7 @@ class Initializer(
* significant amounts of startup time. This value is created using the context passed into
* the constructor.
*/
override val newWalletBirthday: WalletBirthday get() = loadBirthdayFromAssets(appContext)
override val newWalletBirthday: WalletBirthday get() = WalletBirthdayTool.loadNearest(appContext)
/**
* Birthday to use whenever no birthday is known, meaning we have to scan from the first
@ -470,7 +392,7 @@ class Initializer(
* the constructor and it is a different value for mainnet and testnet.
*/
private val saplingBirthday: WalletBirthday get() =
loadBirthdayFromAssets(appContext, ZcashSdk.SAPLING_ACTIVATION_HEIGHT)
WalletBirthdayTool.loadExact(appContext, ZcashSdk.SAPLING_ACTIVATION_HEIGHT)
/**
* Preferences where the birthday is stored.
@ -485,7 +407,7 @@ class Initializer(
override fun hasImportedBirthday(): Boolean = importedBirthdayHeight != null
override fun getBirthday(): Initializer.WalletBirthday {
override fun getBirthday(): WalletBirthday {
return loadBirthdayFromPrefs(prefs).apply { twig("Loaded birthday from prefs: ${this?.height}") } ?: saplingBirthday.apply { twig("returning sapling birthday") }
}
@ -495,7 +417,7 @@ class Initializer(
}
override fun loadBirthday(birthdayHeight: Int) =
loadBirthdayFromAssets(appContext, birthdayHeight)
WalletBirthdayTool.loadNearest(appContext, birthdayHeight)
/**
* Retrieves the birthday-related primitives from the given preference object and then uses
@ -587,67 +509,13 @@ class Initializer(
fun ImportedWalletBirthdayStore(appContext: Context, importedBirthdayHeight: Int?, alias: String = ZcashSdk.DEFAULT_ALIAS): WalletBirthdayStore {
return DefaultBirthdayStore(appContext, alias = alias).apply {
if (importedBirthdayHeight != null) {
saveBirthdayToPrefs(prefs, loadBirthdayFromAssets(appContext, importedBirthdayHeight))
saveBirthdayToPrefs(prefs, WalletBirthdayTool.loadNearest(appContext, importedBirthdayHeight))
} else {
setBirthday(newWalletBirthday)
}
}
}
/**
* Load the given birthday file from the assets of the given context. When no height is
* specified, we default to the file with the greatest name.
*
* @param context the context from which to load assets.
* @param birthdayHeight the height file to look for among the file names.
*
* @return a WalletBirthday that reflects the contents of the file or an exception when
* parsing fails.
*/
fun loadBirthdayFromAssets(
context: Context,
birthdayHeight: Int? = null
): WalletBirthday {
twig("loading birthday from assets: $birthdayHeight")
val treeFiles =
context.assets.list(BIRTHDAY_DIRECTORY)?.apply { sortByDescending { fileName ->
try {
fileName.split('.').first().toInt()
} catch (t: Throwable) {
ZcashSdk.SAPLING_ACTIVATION_HEIGHT
}
} }
if (treeFiles.isNullOrEmpty()) throw BirthdayException.MissingBirthdayFilesException(
BIRTHDAY_DIRECTORY
)
twig("found ${treeFiles.size} sapling tree checkpoints: ${Arrays.toString(treeFiles)}")
val file: String
try {
file = if (birthdayHeight == null) treeFiles.first() else {
treeFiles.first {
it.split(".").first().toInt() <= birthdayHeight
}
}
} catch (t: Throwable) {
throw BirthdayException.BirthdayFileNotFoundException(
BIRTHDAY_DIRECTORY,
birthdayHeight
)
}
try {
val reader = JsonReader(
InputStreamReader(context.assets.open("${BIRTHDAY_DIRECTORY}/$file"))
)
return Gson().fromJson(reader, WalletBirthday::class.java)
} catch (t: Throwable) {
throw BirthdayException.MalformattedBirthdayFilesException(
BIRTHDAY_DIRECTORY,
treeFiles[0]
)
}
}
/*
* Helper functions for using SharedPreferences
*/
@ -738,69 +606,3 @@ internal fun validateAlias(alias: String) {
}
}
/**
* Builder function for constructing a Synchronizer with flexibility for adding custom behavior. The
* Initializer is the only thing required because it takes care of loading the Rust libraries
* properly; everything else has a reasonable default. For a wallet, the most common flow is to
* first call either [Initializer.new] or [Initializer.import] on the first run and then
* [Initializer.open] for all subsequent launches of the wallet. From there, the initializer is
* passed to this function in order to start syncing from where the wallet left off.
*
* The remaining parameters are all optional and they allow a wallet maker to customize any
* subcomponent of the Synchronizer. For example, this function could be used to inject an in-memory
* CompactBlockStore rather than a SQL implementation or a downloader that does not use gRPC:
*
* ```
* val initializer = Initializer(context, host, port).import(seedPhrase, birthdayHeight)
* val synchronizer = Synchronizer(initializer,
* blockStore = MyInMemoryBlockStore(),
* downloader = MyRestfulServiceForBlocks()
* )
* ```
*
* Note: alternatively, all the objects required to build a Synchronizer (the object graph) can be
* supplied by a dependency injection framework like Dagger or Koin. This builder just makes that
* process a bit easier so developers can get started syncing the blockchain without the overhead of
* configuring a bunch of objects, first.
*
* @param initializer the helper that is leveraged for creating all the components that the
* Synchronizer requires. It contains all information necessary to build a synchronizer and it is
* mainly responsible for initializing the databases associated with this synchronizer and loading
* the rust backend.
* @param repository repository of wallet data, providing an interface to the underlying info.
* @param blockStore component responsible for storing compact blocks downloaded from lightwalletd.
* @param service the lightwalletd service that can provide compact blocks and submit transactions.
* @param encoder the component responsible for encoding transactions.
* @param downloader the component responsible for downloading ranges of compact blocks.
* @param txManager the component that manages outbound transactions in order to report which ones are
* still pending, particularly after failed attempts or dropped connectivity. The intent is to help
* monitor outbound transactions status through to completion.
* @param processor the component responsible for processing compact blocks. This is effectively the
* brains of the synchronizer that implements most of the high-level business logic and determines
* the current state of the wallet.
*/
@Suppress("FunctionName")
fun Synchronizer(
initializer: Initializer,
repository: TransactionRepository =
PagedTransactionRepository(initializer.context, 1000, initializer.rustBackend.pathDataDb), // TODO: fix this pagesize bug, small pages should not crash the app. It crashes with: Uncaught Exception: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. and is probably related to FlowPagedList
blockStore: CompactBlockStore = CompactBlockDbStore(initializer.context, initializer.rustBackend.pathCacheDb),
service: LightWalletService = LightWalletGrpcService(initializer.context, initializer.host, initializer.port),
encoder: TransactionEncoder = WalletTransactionEncoder(initializer.rustBackend, repository),
downloader: CompactBlockDownloader = CompactBlockDownloader(service, blockStore),
txManager: OutboundTransactionManager =
PersistentTransactionManager(initializer.context, encoder, service),
processor: CompactBlockProcessor =
CompactBlockProcessor(downloader, repository, initializer.rustBackend, initializer.rustBackend.birthdayHeight)
): Synchronizer {
// call the actual constructor now that all dependencies have been injected
// alternatively, this entire object graph can be supplied by Dagger
// This builder just makes that easier.
return SdkSynchronizer(
repository,
txManager,
processor
)
}

View File

@ -1,17 +1,20 @@
package cash.z.ecc.android.sdk
import android.content.Context
import cash.z.ecc.android.sdk.Synchronizer.Status.*
import cash.z.ecc.android.sdk.block.CompactBlockDbStore
import cash.z.ecc.android.sdk.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.*
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.WalletBalance
import cash.z.ecc.android.sdk.block.CompactBlockStore
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.exception.SynchronizerException
import cash.z.ecc.android.sdk.ext.*
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.transaction.OutboundTransactionManager
import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository
import cash.z.ecc.android.sdk.transaction.PersistentTransactionManager
import cash.z.ecc.android.sdk.transaction.TransactionRepository
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.transaction.*
import cash.z.ecc.android.sdk.validate.AddressType
import cash.z.ecc.android.sdk.validate.AddressType.Shielded
import cash.z.ecc.android.sdk.validate.AddressType.Transparent
@ -39,6 +42,7 @@ import kotlin.coroutines.EmptyCoroutineContext
* data related to this wallet.
*/
@ExperimentalCoroutinesApi
@FlowPreview
class SdkSynchronizer internal constructor(
private val storage: TransactionRepository,
private val txManager: OutboundTransactionManager,
@ -489,4 +493,80 @@ class SdkSynchronizer internal constructor(
serverBranchId?.let { ConsensusBranchId.fromHex(it) }
)
}
interface SdkInitializer {
val context: Context
val rustBackend: RustBackend
val host: String
val port: Int
val alias: String
fun clear()
}
}
/**
* Builder function for constructing a Synchronizer with flexibility for adding custom behavior. The
* Initializer is the only thing required because it takes care of loading the Rust libraries
* properly; everything else has a reasonable default. For a wallet, the most common flow is to
* first call either [Initializer.new] or [Initializer.import] on the first run and then
* [Initializer.open] for all subsequent launches of the wallet. From there, the initializer is
* passed to this function in order to start syncing from where the wallet left off.
*
* The remaining parameters are all optional and they allow a wallet maker to customize any
* subcomponent of the Synchronizer. For example, this function could be used to inject an in-memory
* CompactBlockStore rather than a SQL implementation or a downloader that does not use gRPC:
*
* ```
* val initializer = Initializer(context, host, port).import(seedPhrase, birthdayHeight)
* val synchronizer = Synchronizer(initializer,
* blockStore = MyInMemoryBlockStore(),
* downloader = MyRestfulServiceForBlocks()
* )
* ```
*
* Note: alternatively, all the objects required to build a Synchronizer (the object graph) can be
* supplied by a dependency injection framework like Dagger or Koin. This builder just makes that
* process a bit easier so developers can get started syncing the blockchain without the overhead of
* configuring a bunch of objects, first.
*
* @param initializer the helper that is leveraged for creating all the components that the
* Synchronizer requires. It contains all information necessary to build a synchronizer and it is
* mainly responsible for initializing the databases associated with this synchronizer and loading
* the rust backend.
* @param repository repository of wallet data, providing an interface to the underlying info.
* @param blockStore component responsible for storing compact blocks downloaded from lightwalletd.
* @param service the lightwalletd service that can provide compact blocks and submit transactions.
* @param encoder the component responsible for encoding transactions.
* @param downloader the component responsible for downloading ranges of compact blocks.
* @param txManager the component that manages outbound transactions in order to report which ones are
* still pending, particularly after failed attempts or dropped connectivity. The intent is to help
* monitor outbound transactions status through to completion.
* @param processor the component responsible for processing compact blocks. This is effectively the
* brains of the synchronizer that implements most of the high-level business logic and determines
* the current state of the wallet.
*/
@Suppress("FunctionName")
fun Synchronizer(
initializer: SdkSynchronizer.SdkInitializer,
repository: TransactionRepository =
PagedTransactionRepository(initializer.context, 1000, initializer.rustBackend.pathDataDb), // TODO: fix this pagesize bug, small pages should not crash the app. It crashes with: Uncaught Exception: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. and is probably related to FlowPagedList
blockStore: CompactBlockStore = CompactBlockDbStore(initializer.context, initializer.rustBackend.pathCacheDb),
service: LightWalletService = LightWalletGrpcService(initializer.context, initializer.host, initializer.port),
encoder: TransactionEncoder = WalletTransactionEncoder(initializer.rustBackend, repository),
downloader: CompactBlockDownloader = CompactBlockDownloader(service, blockStore),
txManager: OutboundTransactionManager =
PersistentTransactionManager(initializer.context, encoder, service),
processor: CompactBlockProcessor =
CompactBlockProcessor(downloader, repository, initializer.rustBackend, initializer.rustBackend.birthdayHeight)
): Synchronizer {
// call the actual constructor now that all dependencies have been injected
// alternatively, this entire object graph can be supplied by Dagger
// This builder just makes that easier.
return SdkSynchronizer(
repository,
txManager,
processor
)
}

View File

@ -0,0 +1,267 @@
package cash.z.ecc.android.sdk
import android.content.Context
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.tryWarn
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import java.io.File
import java.lang.IllegalArgumentException
/**
* Simplified Initializer focused on starting from a ViewingKey.
*/
class VkInitializer(appContext: Context, block: Builder.() -> Unit) : SdkSynchronizer.SdkInitializer {
override val context = appContext.applicationContext
override val rustBackend: RustBackend
override val alias: String
override val host: String
override val port: Int
val viewingKeys: Array<out String>
val birthday: WalletBirthdayTool.WalletBirthday
init {
Builder(block).let { builder ->
birthday = builder._birthday
viewingKeys = builder._viewingKeys
alias = builder.alias
host = builder.host
port = builder.port
rustBackend = initRustBackend(birthday)
initMissingDatabases(birthday, *viewingKeys)
}
}
private fun initRustBackend(birthday: WalletBirthdayTool.WalletBirthday): RustBackend {
return RustBackend().init(
cacheDbPath(context, alias),
dataDbPath(context, alias),
"${context.cacheDir.absolutePath}/params",
birthday.height
)
}
private fun initMissingDatabases(
birthday: WalletBirthdayTool.WalletBirthday,
vararg viewingKeys: String
) {
maybeCreateDataDb()
maybeInitBlocksTable(birthday)
maybeInitAccountsTable(*viewingKeys)
}
/**
* Create the dataDb and its table, if it doesn't exist.
*/
private fun maybeCreateDataDb() {
tryWarn("Warning: did not create dataDb. It probably already exists.") {
rustBackend.initDataDb()
twig("Initialized wallet for first run")
}
}
/**
* Initialize the blocks table with the given birthday, if needed.
*/
private fun maybeInitBlocksTable(birthday: WalletBirthdayTool.WalletBirthday) {
tryWarn(
"Warning: did not initialize the blocks table. It probably was already initialized."
) {
rustBackend.initBlocksTable(
birthday.height,
birthday.hash,
birthday.time,
birthday.tree
)
twig("seeded the database with sapling tree at height ${birthday.height}")
}
}
/**
* Initialize the accounts table with the given viewing keys, if needed.
*/
private fun maybeInitAccountsTable(vararg viewingKeys: String) {
tryWarn(
"Warning: did not initialize the accounts table. It probably was already initialized."
) {
rustBackend.initAccountsTable(*viewingKeys)
twig("Initialized the accounts table with ${viewingKeys.size} viewingKey(s)")
}
}
/**
* Delete all local data related to this wallet, as though the wallet was never created on this
* device. Simply put, this call deletes the "cache db" and "data db."
*/
override fun clear() {
rustBackend.clear()
}
//
// Path Helpers
//
/**
* Returns the path to the cache database that would correspond to the given alias.
*
* @param appContext the application context
* @param alias the alias to convert into a database path
*/
fun cacheDbPath(appContext: Context, alias: String): String =
aliasToPath(appContext, alias, ZcashSdk.DB_CACHE_NAME)
/**
* Returns the path to the data database that would correspond to the given alias.
* @param appContext the application context
* @param alias the alias to convert into a database path
*/
fun dataDbPath(appContext: Context, alias: String): String =
aliasToPath(appContext, alias, ZcashSdk.DB_DATA_NAME)
private fun aliasToPath(appContext: Context, alias: String, dbFileName: String): String {
val parentDir: String =
appContext.getDatabasePath("unused.db").parentFile?.absolutePath
?: throw InitializerException.DatabasePathException
val prefix = if (alias.endsWith('_')) alias else "${alias}_"
return File(parentDir, "$prefix$dbFileName").absolutePath
}
/**
* Validate that the alias doesn't contain malicious characters by enforcing simple rules which
* permit the alias to be used as part of a file name for the preferences and databases. This
* enables multiple wallets to exist on one device, which is also helpful for sweeping funds.
*
* @param alias the alias to validate.
*
* @throws IllegalArgumentException whenever the alias is not less than 100 characters or
* contains something other than alphanumeric characters. Underscores are allowed but aliases
* must start with a letter.
*/
internal fun validateAlias(alias: String) {
require(alias.length in 1..99 && alias[0].isLetter()
&& alias.all { it.isLetterOrDigit() || it == '_' }) {
"ERROR: Invalid alias ($alias). For security, the alias must be shorter than 100 " +
"characters and only contain letters, digits or underscores and start with a letter"
}
}
inner class Builder(block: Builder.() -> Unit) {
/* lateinit fields that can be set in multiple ways on this builder */
lateinit var _birthday: WalletBirthdayTool.WalletBirthday
private set
lateinit var _viewingKeys: Array<out String>
private set
/* optional fields with default values */
var alias: String = ZcashSdk.DEFAULT_ALIAS
var host: String = ZcashSdk.DEFAULT_LIGHTWALLETD_HOST
var port: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT
var birthdayHeight: Int? = null
set(value) {
field = value
_birthday = WalletBirthdayTool(context).loadNearest(value)
}
init {
block()
validateAlias(alias)
validateViewingKeys()
validateBirthday()
}
fun viewingKeys(vararg extendedFullViewingKeys: String) {
_viewingKeys = extendedFullViewingKeys
}
fun seed(seed: ByteArray, numberOfAccounts: Int = 1) {
_viewingKeys = DerivationTool.deriveViewingKeys(seed, numberOfAccounts)
}
private fun birthday(walletBirthday: WalletBirthdayTool.WalletBirthday) {
_birthday = walletBirthday
}
/**
* Load the most recent checkpoint available. This is useful for new wallets.
*/
fun newWalletBirthday() {
birthdayHeight = null
}
/**
* Load the birthday checkpoint closest to the given wallet birthday. This is useful when
* importing a pre-existing wallet. It is the same as calling
* `birthdayHeight = importedHeight`.
*/
fun importedWalletBirthday(importedHeight: Int) {
birthdayHeight = importedHeight
}
/**
* Theoretically, the oldest possible birthday a wallet could have. Useful for searching
* all transactions on the chain. In reality, no wallets were born at this height.
*/
fun saplingBirthday() {
birthdayHeight = ZcashSdk.SAPLING_ACTIVATION_HEIGHT
}
//
// Convenience functions
//
fun server(host: String, port: Int) {
this.host = host
this.port = port
}
fun import(seed: ByteArray, birthdayHeight: Int) {
seed(seed)
importedWalletBirthday(birthdayHeight)
}
fun new(seed: ByteArray) {
seed(seed)
newWalletBirthday()
}
//
// Validation helpers
//
private fun validateBirthday() {
require(::_birthday.isInitialized) {
"Birthday is required but was not set on this initializer. Verify that a valid" +
" birthday was provided when creating the Initializer such as" +
" WalletBirthdayTool.loadNearest()"
}
require(_birthday.height >= ZcashSdk.SAPLING_ACTIVATION_HEIGHT) {
"Invalid birthday height of ${_birthday.height}. The birthday height must be at" +
" least the height of Sapling activation on ${ZcashSdk.NETWORK}" +
" (${ZcashSdk.SAPLING_ACTIVATION_HEIGHT})."
}
}
private fun validateViewingKeys() {
require(::_viewingKeys.isInitialized && _viewingKeys.isNotEmpty()) {
"Viewing keys are required. Ensure that the viewing keys or seed have been set" +
" on this Initializer."
}
_viewingKeys.forEach {
DerivationTool.validateViewingKey(it)
}
}
}
}

View File

@ -1,7 +1,5 @@
package cash.z.ecc.android.sdk.exception
import java.lang.RuntimeException
/**
* Marker for all custom exceptions from the SDK. Making it an interface would result in more typing
@ -82,6 +80,14 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE
"Failed to initialize wallet with alias=$alias because its birthday could not be found." +
" Verify the alias or perhaps a new wallet should be created, instead."
)
class ExactBirthdayNotFoundException(height: Int, nearestMatch: Int? = null): BirthdayException(
"Unable to find birthday that exactly matches $height.${
if (nearestMatch != null)
" An exact match was request but the nearest match found was $nearestMatch."
else ""
}"
)
class BirthdayFileNotFoundException(directory: String, height: Int?) : BirthdayException(
"Unable to find birthday file for $height verify that $directory/$height.json exists."
)

View File

@ -7,3 +7,13 @@ internal inline fun <R> tryNull(block: () -> R): R? {
null
}
}
internal inline fun <R> tryWarn(message: String, block: () -> R): R? {
return try {
block()
} catch (t: Throwable) {
twig("$message due to: $t")
return null
}
}

View File

@ -25,6 +25,7 @@ class RustBackend : RustBackendWelding {
internal var birthdayHeight: Int = -1
get() = if (field != -1) field else throw BirthdayException.UninitializedBirthdayException
private set
/**
* Loads the library and initializes path variables. Although it is best to only call this
@ -33,23 +34,27 @@ class RustBackend : RustBackendWelding {
fun init(
cacheDbPath: String,
dataDbPath: String,
paramsPath: String
paramsPath: String,
birthdayHeight: Int? = null
): RustBackend {
twig("Creating RustBackend") {
pathCacheDb = cacheDbPath
pathDataDb = dataDbPath
pathParamsDir = paramsPath
if (birthdayHeight != null) {
this.birthdayHeight = birthdayHeight
}
}
return this
}
fun clear(clearCacheDb: Boolean = true, clearDataDb: Boolean = true) {
if (clearCacheDb) {
twig("Deleting cache database!")
twig("Deleting the cache database!")
File(pathCacheDb).delete()
}
if (clearDataDb) {
twig("Deleting data database!")
twig("Deleting the data database!")
File(pathDataDb).delete()
}
}
@ -61,8 +66,8 @@ class RustBackend : RustBackendWelding {
override fun initDataDb() = initDataDb(pathDataDb)
// override fun initAccountsTable(extfvks: Array<String>) =
// initAccountsTableWithKeys(dbDataPath, extfvks)
override fun initAccountsTable(vararg extfvks: String) =
initAccountsTableWithKeys(pathDataDb, extfvks)
override fun initAccountsTable(
seed: ByteArray,
@ -75,7 +80,6 @@ class RustBackend : RustBackendWelding {
time: Long,
saplingTree: String
): Boolean {
birthdayHeight = height
return initBlocksTable(pathDataDb, height, hash, time, saplingTree)
}
@ -123,22 +127,6 @@ class RustBackend : RustBackendWelding {
"${pathParamsDir}/$OUTPUT_PARAM_FILE_NAME"
)
override fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int) =
deriveExtendedSpendingKeys(seed, numberOfAccounts)
override fun deriveTAddress(seed: ByteArray): String = deriveTransparentAddress(seed)
override fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int) =
deriveExtendedFullViewingKeys(seed, numberOfAccounts)
override fun deriveViewingKey(spendingKey: String) = deriveExtendedFullViewingKey(spendingKey)
override fun deriveAddress(seed: ByteArray, accountIndex: Int) =
deriveAddressFromSeed(seed, accountIndex)
override fun deriveAddress(viewingKey: String) = deriveAddressFromViewingKey(viewingKey)
override fun isValidShieldedAddr(addr: String) = isValidShieldedAddress(addr)
override fun isValidTransparentAddr(addr: String) = isValidTransparentAddress(addr)
@ -207,10 +195,10 @@ class RustBackend : RustBackendWelding {
accounts: Int
): Array<String>
// @JvmStatic private external fun initAccountsTableWithKeys(
// dbDataPath: String,
// extfvk: Array<String>
// )
@JvmStatic private external fun initAccountsTableWithKeys(
dbDataPath: String,
extfvk: Array<out String>
): Boolean
@JvmStatic private external fun initBlocksTable(
dbDataPath: String,
@ -258,20 +246,8 @@ class RustBackend : RustBackendWelding {
@JvmStatic private external fun initLogs()
@JvmStatic private external fun deriveExtendedSpendingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String>
@JvmStatic private external fun deriveExtendedFullViewingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String>
@JvmStatic private external fun deriveExtendedFullViewingKey(spendingKey: String): String
@JvmStatic private external fun deriveAddressFromSeed(seed: ByteArray, accountIndex: Int): String
@JvmStatic private external fun deriveAddressFromViewingKey(key: String): String
@JvmStatic private external fun branchIdForHeight(height: Int): Long
@JvmStatic private external fun parseTransactionDataList(serializedList: ByteArray): ByteArray
@JvmStatic private external fun deriveTransparentAddress(seed: ByteArray): String
}
}

View File

@ -19,23 +19,11 @@ interface RustBackendWelding {
memo: ByteArray? = byteArrayOf()
): Long
fun deriveAddress(viewingKey: String): String
fun deriveAddress(seed: ByteArray, accountIndex: Int = 0): String
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
fun deriveTAddress(seed: ByteArray): String
fun deriveViewingKey(spendingKey: String): String
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
fun decryptAndStoreTransaction(tx: ByteArray)
fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<String>
// fun initAccountsTable(extfvks: Array<ByteArray>, numberOfAccounts: Int)
fun initAccountsTable(vararg extfvks: String): Boolean
fun initBlocksTable(height: Int, hash: String, time: Long, saplingTree: String): Boolean
@ -65,4 +53,18 @@ interface RustBackendWelding {
fun validateCombinedChain(): Int
// Implemented by `DerivationTool`
interface Derivation {
fun deriveShieldedAddress(viewingKey: String): String
fun deriveShieldedAddress(seed: ByteArray, accountIndex: Int = 0): String
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
fun deriveTransparentAddress(seed: ByteArray): String
fun deriveViewingKey(spendingKey: String): String
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
}
}

View File

@ -0,0 +1,129 @@
package cash.z.ecc.android.sdk.tool
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding
class DerivationTool {
companion object : RustBackendWelding.Derivation {
/**
* Given a seed and a number of accounts, return the associated viewing keys.
*
* @param seed the seed from which to derive viewing keys.
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully
* supported so the default value of 1 is recommended.
*
* @return the viewing keys that correspond to the seed, formatted as Strings.
*/
override fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String> =
withRustBackendLoaded {
deriveExtendedFullViewingKeys(seed, numberOfAccounts)
}
/**
* Given a spending key, return the associated viewing key.
*
* @param spendingKey the key from which to derive the viewing key.
*
* @return the viewing key that corresponds to the spending key.
*/
override fun deriveViewingKey(spendingKey: String): String = withRustBackendLoaded {
deriveExtendedFullViewingKey(spendingKey)
}
/**
* Given a seed and a number of accounts, return the associated spending keys.
*
* @param seed the seed from which to derive spending keys.
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully
* supported so the default value of 1 is recommended.
*
* @return the spending keys that correspond to the seed, formatted as Strings.
*/
override fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String> =
withRustBackendLoaded {
deriveExtendedSpendingKeys(seed, numberOfAccounts)
}
/**
* Given a seed and account index, return the associated address.
*
* @param seed the seed from which to derive the address.
* @param accountIndex the index of the account to use for deriving the address. Multiple
* accounts are not fully supported so the default value of 1 is recommended.
*
* @return the address that corresponds to the seed and account index.
*/
override fun deriveShieldedAddress(seed: ByteArray, accountIndex: Int): String =
withRustBackendLoaded {
deriveShieldedAddressFromSeed(seed, accountIndex)
}
/**
* Given a viewing key string, return the associated address.
*
* @param viewingKey the viewing key to use for deriving the address. The viewing key is tied to
* a specific account so no account index is required.
*
* @return the address that corresponds to the viewing key.
*/
override fun deriveShieldedAddress(viewingKey: String): String = withRustBackendLoaded {
deriveShieldedAddressFromViewingKey(viewingKey)
}
// WIP probably shouldn't be used just yet. Why?
// - because we need the private key associated with this seed and this function doesn't return it.
// - the underlying implementation needs to be split out into a few lower-level calls
override fun deriveTransparentAddress(seed: ByteArray): String = withRustBackendLoaded {
deriveTransparentAddressFromSeed(seed)
}
fun validateViewingKey(viewingKey: String) {
// TODO
}
/**
* A helper function to ensure that the Rust libraries are loaded before any code in this
* class attempts to interact with it, indirectly, by invoking JNI functions. It would be
* nice to have an annotation like @UsesSystemLibrary for this
*/
private fun <T> withRustBackendLoaded(block: () -> T): T {
RustBackend.load()
return block()
}
//
// JNI functions
//
@JvmStatic
private external fun deriveExtendedSpendingKeys(
seed: ByteArray,
numberOfAccounts: Int
): Array<String>
@JvmStatic
private external fun deriveExtendedFullViewingKeys(
seed: ByteArray,
numberOfAccounts: Int
): Array<String>
@JvmStatic
private external fun deriveExtendedFullViewingKey(spendingKey: String): String
@JvmStatic
private external fun deriveShieldedAddressFromSeed(
seed: ByteArray,
accountIndex: Int
): String
@JvmStatic
private external fun deriveShieldedAddressFromViewingKey(key: String): String
@JvmStatic
private external fun deriveTransparentAddressFromSeed(seed: ByteArray): String
}
}

View File

@ -0,0 +1,126 @@
package cash.z.ecc.android.sdk.tool
import android.content.Context
import cash.z.ecc.android.sdk.exception.BirthdayException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig
import com.google.gson.Gson
import com.google.gson.stream.JsonReader
import java.io.InputStreamReader
import java.util.*
/**
* Tool for loading checkpoints for the wallet, based on the height at which the wallet was born.
*
* @param appContext needed for loading checkpoints from the app's assets directory.
*/
class WalletBirthdayTool(appContext: Context) {
val context = appContext.applicationContext
/**
* Load the nearest checkpoint to the given birthday height. If null is given, then this
* will load the most recent checkpoint available.
*/
fun loadNearest(birthdayHeight: Int? = null): WalletBirthday {
return loadBirthdayFromAssets(context, birthdayHeight)
}
/**
* Model object for holding a wallet birthday.
*
* @param height the height at the time the wallet was born.
* @param hash the hash of the block at the height.
* @param time the block time at the height. Represented as seconds since the Unix epoch.
* @param tree the sapling tree corresponding to the height.
*/
data class WalletBirthday(
val height: Int = -1,
val hash: String = "",
val time: Long = -1,
val tree: String = ""
)
companion object {
/**
* Directory within the assets folder where birthday data
* (i.e. sapling trees for a given height) can be found.
*/
private const val BIRTHDAY_DIRECTORY = "zcash/saplingtree"
/**
* Load the nearest checkpoint to the given birthday height. If null is given, then this
* will load the most recent checkpoint available.
*/
fun loadNearest(context: Context, birthdayHeight: Int? = null): WalletBirthday {
// TODO: potentially pull from shared preferences first
return loadBirthdayFromAssets(context, birthdayHeight)
}
/**
* Useful for when an exact checkpoint is needed, like for SAPLING_ACTIVATION_HEIGHT. In
* most cases, loading the nearest checkpoint is preferred for privacy reasons.
*/
fun loadExact(context: Context, birthdayHeight: Int) =
loadNearest(context, birthdayHeight).also {
if (it.height != birthdayHeight)
throw BirthdayException.ExactBirthdayNotFoundException(
birthdayHeight,
it.height
)
}
/**
* Load the given birthday file from the assets of the given context. When no height is
* specified, we default to the file with the greatest name.
*
* @param context the context from which to load assets.
* @param birthdayHeight the height file to look for among the file names.
*
* @return a WalletBirthday that reflects the contents of the file or an exception when
* parsing fails.
*/
private fun loadBirthdayFromAssets(
context: Context,
birthdayHeight: Int? = null
): WalletBirthday {
twig("loading birthday from assets: $birthdayHeight")
val treeFiles =
context.assets.list(BIRTHDAY_DIRECTORY)?.apply { sortByDescending { fileName ->
try {
fileName.split('.').first().toInt()
} catch (t: Throwable) {
ZcashSdk.SAPLING_ACTIVATION_HEIGHT
}
} }
if (treeFiles.isNullOrEmpty()) throw BirthdayException.MissingBirthdayFilesException(
BIRTHDAY_DIRECTORY
)
twig("found ${treeFiles.size} sapling tree checkpoints: ${Arrays.toString(treeFiles)}")
val file: String
try {
file = if (birthdayHeight == null) treeFiles.first() else {
treeFiles.first {
it.split(".").first().toInt() <= birthdayHeight
}
}
} catch (t: Throwable) {
throw BirthdayException.BirthdayFileNotFoundException(
BIRTHDAY_DIRECTORY,
birthdayHeight
)
}
try {
val reader = JsonReader(
InputStreamReader(context.assets.open("${BIRTHDAY_DIRECTORY}/$file"))
)
return Gson().fromJson(reader, WalletBirthday::class.java)
} catch (t: Throwable) {
throw BirthdayException.MalformattedBirthdayFilesException(
BIRTHDAY_DIRECTORY,
treeFiles[0]
)
}
}
}
}

View File

@ -171,7 +171,32 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExtendedSpendingKeys(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccountsTableWithKeys(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
extfvks_arr: jobjectArray,
) -> jboolean {
let res = panic::catch_unwind(|| {
let db_data = utils::java_string_to_rust(&env, db_data);
// TODO: avoid all this unwrapping and also surface erros, better
let count = env.get_array_length(extfvks_arr).unwrap();
let extfvks = (0..count)
.map(|i| env.get_object_array_element(extfvks_arr, i))
.map(|jstr| utils::java_string_to_rust(&env, jstr.unwrap().into()))
.map(|vkstr| decode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &vkstr).unwrap().unwrap())
.collect::<Vec<_>>();
match init_accounts_table(&db_data, &extfvks) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while initializing accounts: {}", e)),
}
});
unwrap_exc_or(&env, res, JNI_FALSE)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedSpendingKeys(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
@ -206,7 +231,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExten
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExtendedFullViewingKeys(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedFullViewingKeys(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
@ -241,7 +266,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExten
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddressFromSeed(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveShieldedAddressFromSeed(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
@ -269,7 +294,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddre
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddressFromViewingKey(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveShieldedAddressFromViewingKey(
env: JNIEnv<'_>,
_: JClass<'_>,
extfvk_string: JString<'_>,
@ -303,7 +328,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddre
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExtendedFullViewingKey(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedFullViewingKey(
env: JNIEnv<'_>,
_: JClass<'_>,
extsk_string: JString<'_>,
@ -660,7 +685,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_parseTransa
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveTransparentAddress(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveTransparentAddressFromSeed(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 950000,
"hash": "00000000016bed6cb8b8c2c09791cee821093484fd9ba0f3faad1e78e0d21c17",
"time": 1598439138,
"tree": "018b0ff026a6c3d70e5592d7b42fa829ec41c5a86faa2d12cbbcdf81e039cbee5301f4decddef8e3183dac4de8d1b9aa3918d7df9293aae3c1931f99a253149fb65312019066138ebf0e6cfefbe9cd7876aada7d3f27834d604f827b56b7a56e1a7b551d0000018d080dcdf356c41f5a86d2062ca9e617965faae9b9a7ea678d3799f526a353130001a5ba52f3a03ce38d7d48afc3d2c9e6d65e90dba09b53ad3d3a5921267375de5d016b84dd3e9242073d7069f797bc81a4ccbe0d3847a3b3b4d1376d9e6d61446605015d52a3c83f77ec5c8e823143e74569c0040e1cb58d2a170ce907ae010ba29c670001be5b81f3b1b8a035bd0894392b0b78bdae4c57f206d3875b933de545f65bd93b000000018e1d474609c9c09894638a0ab3e656aadccaf7ddf12bcc6b6ece44a4cc79e1140001f1c57245fff8dbc2d3efe5a0953eafdedeb06e18a3ad4f1e4042ee76623f803200011323ddf890bfd7b94fc609b0d191982cb426b8bf4d900d04709a8b9cb1a27625"
}