commit
188381ef00
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -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.
|
||||
|
|
36
build.gradle
36
build.gradle
|
@ -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'
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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/>   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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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> tx${it.index}:</b> ${it.spendsCount} / ${it.outputsCount}") }
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
rootProject.name = 'zcash-android-wallet-sdk'
|
||||
rootProject.name = 'zcash-android-sdk'
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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()
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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()
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
|
||||
*/
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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."
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 950000,
|
||||
"hash": "00000000016bed6cb8b8c2c09791cee821093484fd9ba0f3faad1e78e0d21c17",
|
||||
"time": 1598439138,
|
||||
"tree": "018b0ff026a6c3d70e5592d7b42fa829ec41c5a86faa2d12cbbcdf81e039cbee5301f4decddef8e3183dac4de8d1b9aa3918d7df9293aae3c1931f99a253149fb65312019066138ebf0e6cfefbe9cd7876aada7d3f27834d604f827b56b7a56e1a7b551d0000018d080dcdf356c41f5a86d2062ca9e617965faae9b9a7ea678d3799f526a353130001a5ba52f3a03ce38d7d48afc3d2c9e6d65e90dba09b53ad3d3a5921267375de5d016b84dd3e9242073d7069f797bc81a4ccbe0d3847a3b3b4d1376d9e6d61446605015d52a3c83f77ec5c8e823143e74569c0040e1cb58d2a170ce907ae010ba29c670001be5b81f3b1b8a035bd0894392b0b78bdae4c57f206d3875b933de545f65bd93b000000018e1d474609c9c09894638a0ab3e656aadccaf7ddf12bcc6b6ece44a4cc79e1140001f1c57245fff8dbc2d3efe5a0953eafdedeb06e18a3ad4f1e4042ee76623f803200011323ddf890bfd7b94fc609b0d191982cb426b8bf4d900d04709a8b9cb1a27625"
|
||||
}
|
Loading…
Reference in New Issue